Repository navigation
Releases
foldyard 0.7.0
A security release: upgrade now. A compromised box could get code run on your computer through
foldyard's own git in fy worktree add and remove — the repo's hooks, its filter drivers, or a
planted partial-clone fetch. foldyard's git on your computer now runs none of them: a new worktree
is checked out, and remove checks it for uncommitted work, in a container from your box image,
so fy worktree add needs fy box build first. After upgrading, recreate each box once
(fy box down && fy box up); on a VM with the guest monitor, or on a Linux or WSL2 host (9p),
also fy machine stop && fy up.
Security
fy worktree addandfy worktree removeno longer run the repo's git hooks on your
computer. The hooks live in the shared.git, which every box can write, and checking out
the new worktree ran itspost-checkouthooks (a consumer's lefthook ones, for instance) on
your computer, in a checkout that wasn't registered or adopted yet. foldyard's own git now runs
with the hooks directory off, each[hook]entry in.git/configor any file it includes
disabled (git 2.54's config hooks, whichcore.hooksPath=/dev/nullalone doesn't stop) and no
core.fsmonitorcommand, andfy worktree addsays which hooks it skipped. Git in the
worktree's box still runs them.fy worktree addno longer checks files out on your computer. A checkout runs the filter
drivers.git/confignames (Git LFS's smudge filter, or a command a box wrote there), and the
shared.gitis writable from every box, so making a worktree could run box-planted code on
your computer. Git there now only records the worktree (its branch and its entry in.git), and
the files are checked out in a one-off container from your box image, with no network.
It therefore needs the box image: before your firstfy box buildit stops with the command to
run, having changed nothing. Git LFS files arrive as pointers (git lfs pullin the worktree's
box fetches them), the firstgit statuson your computer in the new worktree re-reads every
file once, and a checkout that fails removes the half-made worktree and the branch it made.fy worktree removeno longer reads the worktree's files with git on your computer. To tell
whether a worktree had uncommitted work, and to save that work onrefs/fy/removed/<name>-<time>
under--force, git compared or stored every file, and in doing so ran anycleanfilter driver
the shared.git/confignames. A box can write that config, so removing a worktree could run
box-planted code on your computer. For a worktree whose files were checked out in the VM, that
meant every file. The check and the save now run in a one-off container from your box image,
with no network, and git on your computer only writes the ref and deletes the checkout. So
fy worktree removenow needs the box image and the VM. Without them it stops before it has
taken anything down and prints what to run. If you're sure nothing uncommitted in the worktree
matters, the new--discardremoves it without checking or saving anything. A check or save
that fails now keeps the worktree instead of removing it unsaved.- foldyard's own git on your computer no longer fetches. A box can write
.git/configso that
the repo becomes a partial clone, whose missing objects git downloads on demand over a transport
that same config chooses, such as ansshcommand of its own.fy worktree addand
fy worktree removeread objects there, so a missing one could run that command on your
computer. Every git they run now has on-demand fetching and every transport switched off. On a
git older than 2.44, which can't switch the first off, they refuse to work in a repo with a
promisor remote and say to upgrade. That meansextensions.partialClone, or any
remote.<name>.promisororremote.<name>.partialclonefilter, including one in an included
config file. fy worktree remove's "uncommitted work" now means files that differ from the last commit
once everything is unstaged. It used to be git's own check, which also counts the stage. Your
computer's stage for a worktree goes stale whenever its box commits, so a worktree whose files
matched its last commit could still be refused. That no longer happens, and whatever the check
does call uncommitted,--forcesaves. An untracked file counts even when
status.showUntrackedFilesisno. A change that exists only in the stage (staged, then undone
in the file) isn't counted or saved, just as unstaging would lose it.
Deprecated
- The podman backend (
[machine] backend = "podman") is deprecated and will be removed in a
future release. Each project gets its own podman machine, but only one of them can run at a
time, so two projects can't be up at once; and it can't carry the VM firewall. Nothing built on
Lima since reaches it either: no read-only to-box, no from-box (its transcripts stay in the
checkout), no guest monitor. Setbackend = "lima"to move: the nextfy upcreates a new VM,
so images are rebuilt there and the podman machine's volumes, agent logins included, don't come
along.
Changed
fy initwrites a config that matches what fy does today. Step 2 said to uncomment an agent
and runfy box upagain, which does nothing on a running box: an agent is installed when the
box is created, so it now saysfy config adopt, thenfy box down && fy box up. The scaffold
now offers[proxy] no_proxy(the step-3 prompt sends the agent to services by container name,
which hang under the proxy without it),[project] worktree_init, and a commented GitHub block:
an App row and your ownghtoken as agithub-useremergency switch.runtime = "gvisor"is
marked experimental (a test now holds both scaffolds to the experimental register),
host_firewallwarns that committing it refuses colleagues who aren't on Linux, andkeyless
says it belongs infoldyard.local.tomlon a team. In the local example, the private injector
gets abox_envplaceholder, loses a wrong "add it to the allowlist" step and warns that a local
[[inject]]replaces the shared rows;vmtype,[host] notificationsand[claude.settings]
join it.fy initends by pointing atfy prereq. Existingfoldyard.tomlfiles are unchanged.fy tui's Network Log now lists every host in the retained egress log, with refusals you
haven't answered on top. It used to group only the last 400 requests, so an agent's own API
traffic pushed every other host out within minutes — including the refused ones you had to
grant. Each host's tallies now count its whole history (the live log and its rotated backups);
its rows are the newest 50, plus any older refusal, then a line saying how many are earlier.
A host goes back to its place by recency once you grant or decline it.- On Lima, creating a box now moves the old transcripts out of the checkout. 0.6.1 started
binding new boxes' transcripts into foldyard's from-box, but left the old.devbox-claude/and
.devbox-codex/in the checkout with a note that they could be deleted: still matched by
grep -r, and missing fromclaude --resumeinside the new box. The firstfy box upthat
creates a box now copies them into that box's slot and into the archive on your computer, then
deletes them from the checkout, never following a link and only when both copies are complete.
Anything it can't copy whole stays, with a warning, and the next new box tries again. With
DEVBOX_TRANSCRIPTSset, the old place is only archived, as before. - The guest monitor's store now keeps 8 GB on your computer by default, up from 1 GB. A real
project's monitor wrote 21–61 MB an hour during busy batches, and its full 1 GB store reached
back only 15–35 hours, so evidence rolled off before anyone read it. 8 GB is about 106 million
events: 5–16 days of those batches, about six weeks at a steady 30 events a second. The budget
is per project, so each monitored project can take up to 8 GB. Amax_mbin
~/.foldyard/<project>/monitor-store.tomlstill wins (64 MB to 64 GB). - The guest monitor names what isn't a container:
podman,buildand the VM itself. Most
unattributedevents were never containers: on a busy overnight run, 1.28 M of 1.49 M were
podman's own processes (conmon, pasta, crun, netavark) and image builds. They are nowpodman
andbuild, andvmalso covers the VM's sandboxed systemd services. Each label comes from
what the VM's kernel reports and nothing in a container can fake, never from a process's name.
It applies to stored history:fy monitor --since … --until …re-joins it. The first read after
upgrading indexes the new facts from the store (about 20 s on a 330 MB store). - A monitor hook without
whono longer getsvmandpodmanevents, and every batch now
counts what the hook's filters passed over, by label (passed), so nothing is left out
silently. Setskip_unless_askedat the top ofmonitor-hooks.tomlto change the default for
every hook withoutwho, or name a label in a hook'swhoto get it. A process that steps out
of a container into the VM's or podman's namespace is markedcrossed, labelled with the
container it came from, and sent to every hook whatever itswho. - The guest monitor no longer gives a container podman's tools that ran on its namespace number
just before its start or just after its exit. netavark, nft and crun run for an instant in a
namespace of their own that takes the number a container is about to take or has just left (on
one boot, 6,405 events labelled as containers' that weren't). A container's events now run from
its init's start to its exit, plus its own processes killed after that. - Once a process crosses out of a container, monitor hooks get the rest of that VM boot
unfiltered. From the ...
foldyard 0.6.2
Fixed
- A monitor hook no longer stops delivering, without an error, after the upgrade to 0.6.1. On
its first round the new supervisor could read the store while the pull was sealing the older
foldyard's live file, miss the hook's place in it, and report "the store rotated past its
position" when nothing had been removed. It then sent the hook back to the oldest record —
behind events it had already delivered — where it found nothing new to send and never moved
again, whilefy monitor hookandfy doctorshowed ✓. A hook's place is now declared lost
only when the store proves its events gone, the older live file is indexed before it is
sealed, and a hook behind its own cursor moves up to it. Upgrade and runfy host restart:
a hook stuck this way resumes right after the last event it delivered, with no duplicates.
fy monitor hookandfy doctornow warn when a hook's cursor has not moved for ten minutes
while events kept arriving.
foldyard 0.6.1
Monitor hooks no longer drop container events under load, and the monitor keeps far more history:
fy monitor --since/--until reads it back, and each hook batch names the stretches it can't vouch
for. On the Lima backend, a read-only to-box and a box-written from-box now sit beside the checkout:
fy monitor hook add NAME --to-box gives the box a signed event file it can read but not change,
and a new box's transcripts leave the checkout. After upgrading, on the Lima backend run
fy machine stop && fy up once (the VM's mounts changed; a running one is refused until it
restarts), then fy box down && fy box up. On the podman backend, fy host restart is enough.
Changed
- On the lima backend, the VM shares two directories of foldyard's own, and transcripts leave
the checkout. Beside the checkout, the VM now mounts~/.foldyard/<project>/from-box/, which
the box writes, and~/.foldyard/<project>/to-box/, which only your computer writes: the VM and
every box can read it (in the box, at/run/foldyard/to-box), and not even root in the VM can
change it, because the hypervisor's share refuses the write. That holds onvz(virtiofs) and
qemu(9p), so foldyard now pins Lima'smountTypeto match the hypervisor and mounts no to-box
on any other (krunkit). A new box binds Claude's and Codex's transcripts into its checkout's
slot in the from-box instead of.devbox-claude//.devbox-codex/in the checkout, so
git clean -fdxand grep no longer see them andfy worktree removearchives the slot, then
deletes it. For this release the transcript sync reads both places; the next drops the old one.
fy verifychecks the to-box with a real write, andfy doctorhas ato-boxrow.
Every Lima VM needs onefy machine stop && fy up(its mount set changed; until then
foldyard refuses the running VM, and nothing is lost: containers and volumes stay), then
fy box down && fy box upto move a box's transcripts out of the checkout (fy box upwarns
until you do). On the podman backend nothing about the VM changes — its mounts are fixed
when the machine is created, and no podman provider enforces read-only outside the guest — so
there is no restart to do, and boxes keep binding transcripts into the checkout.
FOLDYARD_STATE_DIRmust now point outside the checkout and the worktrees root on Lima
(ADR-0033).
Added
fy monitor hook add NAME --to-boxputs the monitor's signed event file where the box can
read it and can't change it. The file is~/.foldyard/<project>/to-box/monitor/NAME.jsonl,
which every box sees at/run/foldyard/to-box/monitor/NAME.jsonl, read-only. A file in the
checkout could be edited, cut or replaced by the box (the signature found that, but couldn't
stop it). Each line is still signed, so a copy that leaves your computer can still be checked
withfy monitor check. It needs the lima backend onvzorqemu; elsewhere it says why and
points at--file, which works as before. A--fileinside the checkout, the worktrees root
or the from-box now gets a warning that the box can write there.- Experimental: Hook batches say what they can't vouch for. Every batch now carries a
gapslist (usually
empty) naming the stretches of time where evidence is missing or incomplete: the store removed
events before the hook read them, events were lost on the way from the VM, the relay restarted,
a batch was skipped, or events were labelled without the record that would confirm their
container. Mark those times "no evidence", not "nothing happened". Each event also carries
mnt, its mount namespace number. Both are new keys in the samefy.monitor.events/1schema;
a batch can now hold gaps alone (eventsempty,first/lastnull), and file-sink lines
that carry them still passfy monitor check. - Experimental:
fy monitor --since/--untilreads any stretch the store still holds. Give an ISO time (UTC
unless it has an offset) or a span back from now (90s,15m,2h,1d). Events are
labelled as they were when recorded, and missing evidence in the range is listed. - Experimental: The monitor's store keeps hours of activity instead of minutes. Older events are
gzip-compressed (about 30 times smaller) and the store keeps 1 GB on your computer by default
— about 13 million events, where it used to keep 160 MB of uncompressed records, two minutes of
a busy stack start. Setmax_mbin~/.foldyard/<project>/monitor-store.toml(64 MB to 64 GB);
it is never read fromfoldyard.toml, so the box can't shorten what is kept about it.
fy monitorandfy doctorshow how much is kept and how far back. Old files are converted in
place; anything copyinglogs/monitor*.jsonlshould now also takemonitor*.jsonl.gz.
Fixed
- Monitor hooks no longer silently drop container events under load. A hook matched events to
their containers using only the records near its place in the store. When events arrived fast —
a few hundred a second while stacks start, or even 30–40 a second from ten idle containers —
the two records that name the containers were further apart than that, so the events between
were labelledunattributed, awho = ["container"]hook left them out, and nothing said so.
In one measured run a hook lost 5% of its container events, in holes of 10–60 seconds; in
stack-start storms, whole tool calls. Every stretch of the store is now matched against the
records around it wherever they are, so a hook gets whatfy monitorover the whole store
shows. Events dropped before this fix can't be recovered: the store had already removed them.
fy monitorhad the same flaw and is fixed too. Restart the host supervisor
(fy host restart) to pick it up.
foldyard 0.6.0
A security release: upgrade now. Two flaws let a dev box influence what your computer archives from
its agent transcripts: a planted link could make it copy other files on your computer into the
archive, and a planted session could be resumed by an agent on your computer in another project.
Both are fixed, and box Codex sessions now archive into foldyard's own Codex home instead of
~/.codex/sessions. After upgrading, on the Lima backend run fy machine stop && fy up once: every
Lima VM's boot provisioning changed, walled or not, and a running one is refused until it restarts
(the podman backend needs no restart). Then fy box down && fy box up, so the box picks up this
release.
Security
- Archiving transcripts never follows a link out of the checkout. The bound-out transcript
dirs (.devbox-claude/projects,.devbox-codex/sessions) live in the checkout, which the box
can write, and the host copied them into your history (~/.claude/projects,~/.codex/sessions)
with rsync — which follows a symlink in the source path. A box could therefore makefy box down,
fy nuke,fy worktree remove,fy transcriptsor thetranscript_sync_secondssweep copy any
directory you can read on your computer into that history. The copy is now foldyard's own walk,
which opens every directory on the way and every entry inside without following a link: a link
on the way to the transcript dir refuses the archive and names the path; one inside it is
skipped, as are pipes and other special files. Otherwise it behaves as before (additive, newer
copies win, mtimes kept), writes new archive files private to you, skips a single file over 4 GiB
and stops copying before the archive's disk falls under 1 GiB free. rsync is no longer needed on
your computer. One change fortranscript_sync_seconds: a file the sweep cannot copy (unreadable,
too large, no room) is now reported once, like any other failure, instead of passing silently. - Archiving Claude transcripts copies only the checkout's own project folder. Claude Code
files each session in a folder named after the directory it started in, and a Claude on your
computer resumes the folder named after its own directory. The box chooses those folder names,
so the archive (fy box down,fy nuke,fy worktree remove,fy transcripts, the
transcript_sync_secondssweep) could place box-written sessions whereclaude --resumein a
different project on your computer would load them. Now only the folder named after the
checkout itself is archived (each worktree archives its own); every other entry is left in the
bound-out dir and named — printed byfy transcriptsand box down, logged once by the sweep.
Sessions the box starts in a subdirectory of the checkout are left out with them. Codex
records a session's directory inside each file rather than in a folder name, so its sessions
can't be held to a folder this way; they no longer go into~/.codex/sessionsat all (below). - Box Codex sessions are no longer archived into
~/.codex/sessionson your computer. Codex
records a session's working directory inside the session file, which the host can't check, so
a session the box wrote could claim another project's directory and be offered by
codex resumewhen you worked in that project.fy transcripts,fy box down,fy nuke,
fy worktree removeand thetranscript_sync_secondssweep now archive them into a Codex
home of foldyard's own, one per project:~/.foldyard/<project>/transcripts/codex. There is no
setting that puts them back in~/.codex. To resume one, run
CODEX_HOME=~/.foldyard/<project>/transcripts/codex codex resumefrom the checkout after
logging in there once;fy transcriptsprints the line. An explicit archive dir
(fy transcripts <dir>orFOLDYARD_TRANSCRIPTS_ARCHIVE) still files them under
codex-sessions/. Sessions earlier versions archived into~/.codex/sessionsstay where they
are — foldyard never moves or deletes them; each also sits under the same name in the
checkout's.devbox-codex/sessionsunless that was cleaned, and the next archive copies it to
the new place too.
Changed
- Two injectors may share a host when they write different headers, and the proxy routes each
request by the placeholder it carries. Keyless Claude on its OAuth token (authorizationon
api.anthropic.com) and an[[inject]]row writingx-api-keythere, with abox_env
placeholder, can now both be on, so an evaluation harness in the box can call the Anthropic API
with its own key beside Claude Code. Before,fy moderefused the pair, because the proxy injected
the first rule matching a host and path whatever the request carried. Now, where several rules
match, it injects only the one whose placeholder the request carries in that rule's header, and
answers a request carrying neither or both with a 502 naming the switches (the Network Log marks
itno credential named). A host with one rule injects as before, except that a request
carrying only another credential's placeholder, in another header, is never sent on the rule's
credential: it gets that switch's "turn it on" answer if it is off, or a 502 saying the path is
outside that credential'spath_prefix(markedplaceholder out of scope). Two injectors
writing the same header, or one whose row has nobox_env, are still refused. Give the row's placeholder
a variable only the harness reads: Claude Code prefersANTHROPIC_API_KEYover its OAuth token.
Seefy docs configuration. fy worktree addsuggests a directory name when given a branch-shaped one. The first
argument is the worktree's directory, sofy worktree add sec/vuln-bumpsis refused (a nested
directory would never be discovered). The refusal now suggests
fy worktree add sec-vuln-bumps sec/vuln-bumpsinstead of leaving you to work out which
argument was wrong.fy claudestarts a fresh box straight at the prompt. Claude Code used to ask two
questions first: whether to trust the folder (where Enter picks "No, exit") and whether to
accept bypass-permissions mode, whichfy claudealways launches in. It now answers both in the
box's own Claude config, for the checkout you launch from. Trusting the folder is safe because
the box is the boundary: the repo's hooks and MCP servers reach no further than the agent
itself. The theme now starts onauto, which follows your terminal's light or dark background.
All three are defaults: a theme you pick with/themestays.- The VM's boot scripts now say "VM firewall" and "host" instead of "wall" and "Mac". Their
comments, messages, the guest'sfy-wallunit descriptions and one nft rule comment changed;
no rule, path or unit name did. Every byte of those scripts is in the boot provisioning a Lima
VM records, and the firewall script is recorded with the firewall off too (itsuninstall
runs then). So every Lima VM, with or without[machine] firewall, is refused as stale once:
fy upstops until you runfy machine stop && fy up.
Added
- The docs and both bundled skills explain why a cache prune can fail with
Permission deniedin a shared volume. On the VM's SELinux guest, a file copied into a volume can keep
the private label of the container that copied it, and once that container is recreated no
other container can delete it.fy docs configuration(under[reclaim]) gives the fix. To
refresh a copy installed in your repo, runfy skill install --force foldyardand
fy skill install --force bootstrap-devbox. - Experimental:
fy monitorshows what the guest monitor recorded, labelled with each event's container
and worktree. A root relay in the VM signs every event. Your computer pulls them every few
seconds over the VM's ssh and keeps only lines that verify, counting any that went missing or
were altered (fy doctor'smonitor eventsrow). Containers are identified from the kernel's
own records, not from the container id Tetragon guesses, which a container's creator can forge.
Names and worktrees are what the engine reports, and are shown as claims. - Experimental: Guest monitor hooks send batches of attributed events to your own program or endpoint.
Configure them in~/.foldyard/<project>/monitor-hooks.tomlon your computer, never in
foldyard.toml, because anything in the box can write the checkout. A command gets each batch
as JSON on stdin with a minimal environment, sohost.envsecrets are not passed. A URL gets it
as a POST. Events are sent once their attribution is settled. Delivery is at least once,
retried with backoff, and a batch is skipped and counted after five failures. A hook that falls
behind catches up from where it stopped; if the event store rotates past it first, the
supervisor log says so. - Experimental:
[machine] monitor = "observe"runs a guest monitor — Tetragon, a root eBPF collector, in
the lima VM. Experimental means off unless you turn it on, and its event format,
fy monitor --jsonoutput and hook payload may change in a minor release while it settles. It
records process starts, outbound TCP connections and access to selected credential
files for every container and the VM itself, and blocks nothing. A pinned, checksummed release
is downloaded on your computer, copied into the VM, and checked again as root before it is
installed. The box's user can't stop it, change its policy or read its log.fy doctorshows
whether it is observing. Off by default; changing it needsfy machine stop && fy up
(ADR-0032). - Five new pages in
fy docs:workshop,stack,git,github-appandgcp. Running a
workshop (what attendees set up beforehand on macOS, Linux and Windows via WSL2, and the order
on the day); bringing a compose stack in, including p...
foldyard 0.5.0
Every VM needs one fy machine stop && fy up after upgrading: the VM's boot setup changed (the
proxy settings it gives build steps and stack containers), and until it restarts foldyard refuses
a running Lima VM as stale. No config edits are needed. Outside a foldyard project, project verbs
now refuse instead of running as a project named after the directory. New: fy prereq checks
what your computer needs, and fy prereq --fetch bakes a base VM that new projects clone in
seconds; [box].packages bakes system packages into the box image once; and fy changelog
prints what changed since the foldyard your foldyard.toml declares.
Changed
- Outside a foldyard project,
fysays so instead of inventing one. With nofoldyard.toml
in the current directory or above (and noFOLDYARD_REPO), a project verb (fy mode,
fy doctor,fy up,fy host…) used to run as a project named after the directory, writing
a mode file and allocating a port band for it in~/.foldyard/ports.json. It now prints one
line pointing atfy initandfy prereq, exits 1 and writes nothing. That includes a git
repo that has nofoldyard.toml. - Help,
fy prereq,fy docs,fy init,fy machine baseandfy skill listwork anywhere,
quietly. Every verb,--helpincluded, used to open with "✗ not a git repository" outside a
git repo, includinguvx foldyard prereqin an empty directory. - A refused request in the Network Log now shows what was asked. A blocked row used to read
only "⛔ refused by the allowlist". Now it shows the method and target:CONNECT host:portfor
HTTPS (refused before TLS, so the path is never seen), and the method and path for plain HTTP.
The text reads "blocked — not on the allowlist". A request refused because its Host header
named a different host than the one it was sent to now says so, instead of blaming the
allowlist. It is also no longer offered as a host to grant, since a grant would not let it
through. The egress log's blocked rows carrymethod,pathandmismatchto match. - A refused HTTPS request now shows its method, path and User-Agent in the Network Log, and a
refused host is never connected to. The proxy used to refuse an ungranted host atCONNECT,
before TLS, so the row could only sayCONNECT host:443. It now answers theCONNECTitself,
decrypts the tunnel and returns the403to the request inside. No connection to the refused
host is ever opened: the proxy also refuses, at the point where every upstream connection is
made, any connection to a host nothing grants. Previously a separate check on each path made
that guarantee, so it now holds in one place. Unchanged: a port other than:443and an image
build are still refused atCONNECT. One visible difference: a client in the box that keeps
its own trust store (it doesn't trust the proxy's CA) now gets a certificate error for a
refused host instead of the readable403. The log still records it as a blockedCONNECT.
Added
fy prereqchecks what your computer needs and prints how to install it. Lima (and its
version), podman, a hypervisor, and on Linux QEMU,/dev/kvmand asystemd --usersession,
plus disk, memory and foldyard's own egress proxy. Each missing piece comes with the install
command for the package manager you have; foldyard never runs it. It needs no project, so
uvx foldyard prereqworks before foldyard is installed, and it exits non-zero until
everything required is there.- New projects' VMs are clones of a base VM: seconds instead of minutes, and almost nothing
downloaded.fy prereq --fetch(orfy machine base bake) bakes a project-less base VM once:
Lima's podman template with nothing mounted, booted, the generic box image built in it. After
that,fy up/fy box upcreate a new project's VM as alimactl cloneof it with the
project's own sizing and mounts, andfy box upre-tags the box image it carries in instead of
building one. From an empty directory to a walled box took 26 s on macOS. Existing VMs are
never re-cloned; a base out of date with Lima's template is not used, and one over 30 days old
is flagged.FOLDYARD_BASE_VM=0turns cloning off.fy docs base-vmhas the rules. - Node's
fetch()and npm's install scripts go through the proxy without project wiring.
Node'sfetch()ignoresHTTPS_PROXYunlessNODE_USE_ENV_PROXYis set, so under the VM
firewall it failed with anECONNREFUSEDagainst a raw IP that named no proxy. An install
script that fetches with its own client (supabase's CLI download) reads only
npm_config_https_proxy. The box now getsNODE_USE_ENV_PROXY=1,npm_config_https_proxy
andnpm_config_proxy(defaults:[box].envmay override each), and with the VM firewall on
every stack container gets the two npm settings. Stack containers don't get
NODE_USE_ENV_PROXY: it would send calls between Node services to the proxy, which can't
resolve a service name. An app container that wants it sets it itself, with aNO_PROXY
naming the stack's services (fy docs networking, "Node and npm"). The stack containers'
settings are part of the VM's boot setup, sofy uprefuses a running VM until you run
fy machine stop && fy uponce. fy doctorpoints out compose settings that override what foldyard provides. With the VM
firewall on, foldyard gives every container the proxy settings, the proxy's CA and npm's proxy
settings, and a value your compose file sets wins without a word, including a proxy address
that has gone stale or a mount of.devbox-ca. The newstack env overridesrow names each
one per service. It's advisory: a value you need, such as a private CA bundle, can stay. Empty
values aren't flagged, since blanking the proxy is how a stack-only service stays off it. If
your project wired any of these by hand before foldyard provided them, you can drop them now.
A second row,stack container env, reads what each running container actually has, so it
also catches an image's ownENV(anSSL_CERT_FILEof its own) and a container created
before the VM provided these settings, which keeps the proxy but lacks the CA until you
recreate it (fy down, thenfy up). Containers that blank the proxy aren't flagged there
either.- The networking guide says when stack containers trust the proxy, and how to fix Turbopack.
Only the VM firewall gives containers the proxy's CA, so a container you point at the proxy
yourself needs it on.next buildfetching Google Fonts ignoresNODE_EXTRA_CA_CERTS;
NEXT_TURBOPACK_EXPERIMENTAL_USE_SYSTEM_TLS_CERTS=1makes it readSSL_CERT_FILE. [box].packagesinstalls system packages once, not on every new box. List them
(packages = ["nodejs", "just"]) and foldyard bakes them into a cached layer on top of the box
image, rebuilt only when the list or the base image changes (or byfy box build). Until now
the only way to add a package to the packaged box was a[[box.tools]]step, which reinstalled
it, through the egress wall, every time a box was created. The layer uses whatever package
manager the image has (apt, apk, dnf, microdnf, yum, zypper, pacman), so it works on your own
[box].imagetoo. A box that is already running keeps its old image until
fy box down && fy box up.fy changelogprints what changed between the foldyard yourfoldyard.tomldeclares and the
one installed. A project can sit any number of releases behind thefyrunning it, and
catching its config up meant reading the whole changelog on GitHub. Now the releases in between
(aftermin_foldyard_version, orrecommended_foldyard_versionif higher, up to the installed
version) print oldest first, from the changelog the install itself ships, so it works offline
and inside the box.--actionkeeps only what can need a step (each release's summary plus
Security, Removed, Deprecated and Changed);--since/--untilpick the range by hand and work
outside a project. Raisemin_foldyard_versiononce the config is caught up and the next run
starts there.fy doctor's version row points at it when the install is ahead of the repo.- A project that declares no foldyard version still gets a starting point:
fy changelog
falls back to the datefoldyard.tomlwas last modified and shows the releases from then,
labelled as an estimate (a clone or checkout rewrites the file's time, so it can start late).
--sincealso takes a date (--since 2026-09-01) to set it by hand.
Fixed
fy verify's push check works in a walled box, and on a public origin. Since 0.3.2 its
gitran with the compose client's environment, which carries no proxy: in a box behind the
VM firewall git went direct, was refused, and every origin read "git remote UNREACHABLE … push
refusal UNPROVEN". It now runs with the box's own environment, as every other box check does.
And the probe is a dry-run push (git push --dry-run, which sends nothing) instead of
git ls-remote: a read proves nothing about pushing — a public origin answers it with no
credential, which made the row FAIL on every public repo — while the push handshake needs write
access on any repo.fy logsworks on the podman engine. It asked compose for the last 50 lines with-n 50,
which docker compose reads as--tail 50but the bundled podman-compose reads as its
value-less--namesswitch followed by a service called50, so everyfy logs [svc]on
podman failed with "missing services [50]". It now sends--tail 50, which both read the same.
A new test hands every compose command foldyard sends to podman-compose's own argument parser,
so a flag the two providers read differently fails the suite instead of a user's command.- **
fy up,fy box upandfy box buildwait for the host supervisor's daemons, and refuse
when one ...
foldyard 0.4.0
Every VM needs one fy machine stop && fy up after upgrading: the VM's boot script changed, and
until it restarts foldyard refuses a running Lima VM (the default backend) as stale. Three changes can need a config edit,
each spelled out under Changed: [plugins.github] becomes [[inject]] rows (its App key
moves to FY_INJECT_GITHUB in host.env), [proxy] passthrough now defaults to
["@jvm", "@linux"] instead of ["@all"], and [box].env refuses the names foldyard sets in the
box.
Security
- Your computer runs no git at all to heal the shared index. 0.3.2 hardened the supervisor's
git calls against a box-written.git/config; now there are none. The box's git shim builds the
healed index and leaves it next to the shared one, and the supervisor only installs it — under
git's own lock, and only if neither the index nor HEAD changed since, so nothing staged on your
computer is ever lost. Nothing to migrate: a box on the new shim is picked up byfy box up;
until then a stale host-sidegit statusis fixed bygit reset, as before the heal existed.
(Follow-up to GHSA-j5mq-v7p4-qw2j; ADR-0021.) - A program in the box can no longer hide what it fetches behind the image-build marker. With
the VM firewall on, image builds reach the proxy as userfy-build, and the proxy tunnels them
instead of decrypting them. Anyone could use that user, so the box could too: putting
fy-build:<anything>@in its own proxy URL turned a logged request (path, tool) into a
hostname-only row. The allowlist still applied. Now only a build carrying the secret foldyard
creates for it on your computer is tunnelled; a build started inside the box (fy upthere)
is decrypted and logged like any box request, and works because build steps now trust the
proxy's CA.
Removed
[plugins.github]is gone: GitHub is[[inject]]rows now, andfy uprefuses the old
table until it's replaced. The refusal prints the rows to paste, computed from your table: a
kind = "github-app"row (yourapp_id,installation_id,repoasrepositories, and
box_env = { GH_TOKEN = "x" }) and, commented out, akind = "gh-cli"emergency row for the old
userlevel. Three things move with it. The App's private key is read from the switch's
FY_INJECT_<SWITCH>inhost.env(FY_INJECT_GITHUBforswitch = "github") — rename
GH_PEM_B64, same value, and any[[secret]]row naming it.fy mode github=appis
fy mode github=on, and the oldgithub=useris the gh-cli row's own switch. And
permissionshas no successor: the token now carries the App installation's own
permissions — foldyard no longer narrows it topull_requests/issuesor refuses a map that
widens it, which is what turned a consumer's grantedactions: readinto a morning of 401s
(ADR-0031).
To change what the box can do on GitHub, change the App; for a second scope, a second App with
its own row. foldyard no longer installsghin the box: put it in your box image.
Changed
uv tool install foldyardis now the complete install on your computer — no[host]extra
needed. mitmproxy (the egress proxy) and PyJWT (the GitHub App minter) are ordinary
dependencies now, so the install you get by default is the one that works, and an upgrade
can no longer quietly leave the proxy out. The box, which only routes through the proxy, is
installed byfy box upwith both removed, as before."foldyard[host]"still installs (the
extra is kept, empty), so existing instructions and installs keep working.- Two switches can no longer both inject on one host and path.
fy mode(and the TUI's mode
buttons) refuse a level whose injection rules would overlap another switch's active ones (the
same host, with path prefixes where one is under the other or either covers the whole host). The
message names the other switch and the exactfy modecommand to run first. Before, a consumer's
[[inject]]row onapi.anthropic.combeside keylessclaude=onboth applied, and the proxy
sent whichever credential's rule came first in plugin load order. One switch may still split a
host by path, as Codex on a ChatGPT subscription does. The check runs on the rules the proxy
would actually get, so a rule that only appears with several switches on together is caught too. If your saved mode already has such a
pair on (or you adopt an[[inject]]row that creates one), the proxy injects neither until
one is off, and says so: a supervisor log line and notification, an error row infy modeand
fy state, acredential overlaprow infy doctor, and both rows markedHELD BACKin
fy config widenings, with the commands that end it. - A declared credential routes the box through the proxy, even while its switch is off. An
[[inject]]row, or keyless Claude or Codex, without a[proxy]table used to leave a box
created with that switch off with no proxy route and no placeholder, so switching it on later
couldn't reach the box untilfy box down && fy box up. The box now gets the route and its
box_envplaceholders whenever a credential is declared, and the proxy listener runs for it. - The proxy now decrypts the toolchain too:
[proxy] passthroughdefaults to
["@jvm", "@linux"]instead of["@all"]. With the VM firewall on, the box, your stack's
containers, image build steps and the VM's image pulls all trust the proxy's CA, and decrypting
a toolchain install measured within a few per cent of tunnelling it, sonpm,pip/uv,
cargo,go,git, image pulls, the Claude Code installer and gcloud are now logged request by
request. Only Java's own trust store (@jvm) andaptover HTTPS (@linux) stay tunnelled. A
project that setspassthroughitself is unaffected. To get the old behaviour back, set
passthrough = ["@all"](or add just the bundle a tool needs, e.g."@vcs"), then adopt it at
the nextfy up. The sign that a tool can't work decrypted is a certificate error naming the
proxy's CA (mitmproxy, e.g. Java's "PKIX path building failed" or Chromium's
ERR_CERT_AUTHORITY_INVALID). The box also setsCLOUDSDK_CORE_CUSTOM_CA_CERTS_FILE, so
gsutilandbqtrust the proxy likegclouddoes. [box].envis your project's environment, and refuses the names foldyard sets in the box.
fy box upnow stops before starting anything when[box].envsets a name foldyard owns:FY_*,
FOLDYARD_*, the proxy and CA variables (HTTPS_PROXY,NO_PROXY,SSL_CERT_FILE, …), the
engine socket (CONTAINER_HOST,DOCKER_HOST,DOCKER_CONFIG), the agents' homes and the
shell basics such asPATH. Before, such a name silently replaced the proxy, CA or engine
wiring, or was silently ignored, and the adoption diff showed it as one harmless-looking line.
The message names the setting to use instead where there is one ([box] clean_docker_config,
[box] git_index_split,[proxy] no_proxy,fy allow add).TZandLC_TIMEstay yours to
set. A key that isn't a plain environment-variable name is refused too: a quoted
"HTTPS_PROXY=x"would otherwise have setHTTPS_PROXY. An[[inject]]row'sbox_env
already refused the same names.
Added
fy doctorin the box checks that box git is foldyard's git shim. Every box-side git
protection, from the separate index to the checks that stop the box moving a branch your
computer just moved, lives in the shim at/usr/local/bin/git. A box image that puts another
git first onPATH, or a shim whose hooks didn't install, used to switch all of that off with
no sign. Two new rows say so.git shimfails whengitisn't the shim, andgit shim hooks
fails when its hooks are missing. It warns when a user other than root could rewrite them. Each
row names its fix. No rows when[box] git_index_split = false. A new box now runsfy doctor
once when it's created, as the last lines offy box up, so it reports this without being
asked. (ADR-0021.) Itsegress proxy CArow now says the box must be recreated too: the CA
is mounted when a box is created, sofy host restartalone never reached an existing box.- foldyard shows what a GitHub App installation grants, and tells you when that changes.
foldyard no longer caps agithub-apptoken's scope (the installation's permissions are the
box's scope), so it reports them instead. While the switch is on, its probe reads the
installation's permissions and which repositories it covers, as the App, minting nothing.
fy config wideningsshows the last reading under the row and how old it is, flagging any
writeoradminpermission, and any level foldyard doesn't recognise as if it were one
("not yet probed" until the switch has been on), andfy modeandfy tuishow it beside the
switch. All of them read a record on your computer and never call GitHub. When a reading
differs from the last one, for example after an org owner accepted a new permission on
github.com, the supervisor logs one line naming what was added, removed or changed level and
sends one desktop notification; the first reading is a baseline. When a probe can't read the
scope, the last reading stays, marked as possibly stale in those reports, and the supervisor
logs that once. An uninstalled or suspended installation now shows the switch as DEGRADED,
naminginstallation_id. - With an
[[inject]]switch off, a request carrying itsbox_envdummy gets an answer naming
the fix instead of the provider's "Bad credentials". The proxy answers it itself with a 401
whose JSONmessagesays the switch is off and to runfy mode <switch>=onon your computer,
whichghand most clients print. It matches where the row's rule would inject (host,
path_prefix, header orquery_param), and in a header the dummy may stand alone or follow one
auth scheme (`g...