Repository navigation
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 podman-compose's differences from docker
compose; git when the box and your computer share a checkout; setting up a GitHub App for
[[inject]] kind = "github-app"; and Google Cloud access through the metadata emulator. The
manual pages now carry diagrams (on GitHub), and the quickstart's agent step is corrected: on a
running box, adopt the config and runfy box down && fy box up—fy box upalone only warns. - The manual is online at twistco.github.io/foldyard,
and the README opens with a demo. The site renders the same pagesfy docsships, plus the
design record and the contributor pages, with search and the diagrams. The demo is a GIF of the
quick start (foldyard initthroughfy verifyand an edit your computer holds until you approve
it), recorded by CI against each release on a real VM. - Experimental: Each guest monitor event names the process that started it, where Tetragon reports one.
fy monitor --jsonand hook payloads gainppid,parentandparent_args(the parent's id,
binary and arguments) andexec_id/parent_exec_id(the links to rebuild the whole process
tree fromexecevents), all taken from what Tetragon already reports. Without them a consumer
could only attribute by time, and a container's own daemon was blamed on whatever a session was
doing: under BashArena, a firewall daemon's reload runningiptables -Fscored the same against
an honest run as an attack. The fields are additive (the schema staysfy.monitor.events/1) and
empty on events that lack them;fy monitor's own output is unchanged. - Settings foldyard isn't ready to stand behind are now marked (experimental) in the
configuration docs, andfy doctorlists the ones your config turns on. Each says which of
two things it means, or both: its config or output may still change, or it is settled but
unproven in day-to-day use.[machine] runtime = "gvisor"and[machine] vmtype = "krunkit",
both released before, are now marked; they work as before. A setting stops being experimental in
a release whose changelog says so. - Experimental: A monitor hook can write to a file, and
fy monitor hook addsets one up. Use
fy monitor hook add NAME --file PATH(or--url URL, or-- /abs/program args…), with
--who,--kindsand--worktreefilters. Barefy monitor hooklists your hooks and how
their deliveries are going,fy monitor hook removeremoves one, andfy doctorwarns about a
hook that is failing or has skipped events. Before this, hooks had to be written by hand in
~/.foldyard/<project>/monitor-hooks.toml, and nothing showed whether they worked. Afile
hook replaces a shell command that appends into the checkout so the box can read the events.
Such a command follows symlinks, so the box could aim your computer's writes at any of your
files. The sink never follows a symlink the box could have made. It signs each line with a key
only your computer holds, sofy monitor check PATHfinds a line edited, removed, moved or
added, or lines cut from the end. Barefy monitoris unchanged.
Fixed
- A long-running box no longer crashes its agents. The box's first process was
sleep infinity, which never collects the processes left to it, and everygit commitleaves
one behind (git's background maintenance). They piled up for the box's whole life, and once
the box reached its limit of 2048 processes, a program that couldn't start a thread aborted:
Claude sessions died with "core dumped", several at once. A box is now created with--init,
so they are collected as they exit. A running box keeps the old behaviour until
fy box down && fy box up. - A
[reclaim] scriptruns by its own shebang, and a failed one is reported. foldyard ran it
assh <script>, so a bash script ran under the box's dash and died at its first bash-ism
before any of its sweeps ran; the exit code was ignored, so a single stderr line was the only
sign. An executable script now runs by its shebang (a file without the executable bit still
runs undersh), and a non-zero exit prints a⚠line. It still doesn't stopfy up.
If your scriptevalsfy shellenv, remove that: shellenv's output is bash and is meant for
recipes. - On a host that exports
HTTPS_PROXY, a walled Lima VM's image pulls no longer fail. Lima
copies the host's proxy variables into the guest at each start (propagateProxyEnv), where
they overrode the VM firewall's own proxy: the guest's podman pulled through the host's proxy,
and the firewall refused every pull (proxyconnect tcp: dial tcp 192.168.5.2:…: connection refused). With[machine] firewallon, foldyard now recordspropagateProxyEnv: false
before the VM boots; without the firewall the host's proxy stays the guest's. A VM already
running with the copied proxy is warned about once, and needsfy machine stop && fy up. - In a walled VM, apt in a docker-compose build reaches the proxy. Compose's classic builder
(what it uses against podman, which serves no BuildKit) passes only the upper-case
HTTP_PROXY/HTTPS_PROXYbuild args, and apt reads onlyhttp_proxy. So an aptRUNstep
in a compose build started from the box went direct, and the VM firewall refused it. The
guest's build-step hook, which already adds foldyard's CA to every build step, now also adds
each proxy variable in the spelling the step lacks. This changes the VM's boot provisioning:
every running Lima VM, walled or not, is refused as stale untilfy machine stop && fy up. fy box shellnow tells the box what terminal you're using. The engine'sexec -tset
TERM=xtermand nothing else, so tools in the box saw a 16-colour terminal: Claude Code mapped
its theme onto your terminal's basic palette, where the selected option in a menu could all but
vanish on a light background. Each attach now passes your computer'sTERM,COLORTERM,
TERM_PROGRAM,TERM_PROGRAM_VERSIONandCOLORFGBG(whichever are set), and falls back to
xterm-256colorwhen the box has no terminfo for yourTERM(Ghostty'sxterm-ghostty,
kitty'sxterm-kitty).- A running box whose environment no longer matches the config now says so. A box's
environment is fixed when it's created, so an[[inject]]row, a[box].enventry or a
timezone change adopted afterwards never reached it — andfy box upprinted "already up" while
the new variable was simply absent.fy box upon a running box now prints one warning naming
each variable that's missing, changed or no longer set (values only forTZandLC_TIME),
with the fix:fy box down && fy box up.fy mode,fy stateand the TUI make the same
comparison, which used to cover only the keyless Claude and Codex dummies, and their fix line
saidfy box up, which keeps a running box as it is. A timezone or time-locale change on your
computer is shown too, with its old → new value, but only as a warning:fy statestill passes
on it. It exits non-zero when any other variable no longer matches, or when aTZorLC_TIME
pinned in[box].envchanged (that is config).