Skip to content

Releases

foldyard 0.7.0

Choose a tag to compare

@github-actions github-actions released this 27 Oct 20:14

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 add and fy worktree remove no 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 its post-checkout hooks (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/config or any file it includes
    disabled (git 2.54's config hooks, which core.hooksPath=/dev/null alone doesn't stop) and no
    core.fsmonitor command, and fy worktree add says which hooks it skipped. Git in the
    worktree's box still runs them.
  • fy worktree add no longer checks files out on your computer. A checkout runs the filter
    drivers .git/config names (Git LFS's smudge filter, or a command a box wrote there), and the
    shared .git is 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 first fy box build it stops with the command to
    run, having changed nothing. Git LFS files arrive as pointers (git lfs pull in the worktree's
    box fetches them), the first git status on 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 remove no longer reads the worktree's files with git on your computer. To tell
    whether a worktree had uncommitted work, and to save that work on refs/fy/removed/<name>-<time>
    under --force, git compared or stored every file, and in doing so ran any clean filter driver
    the shared .git/config names. 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 remove now 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 --discard removes 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/config so that
    the repo becomes a partial clone, whose missing objects git downloads on demand over a transport
    that same config chooses, such as an ssh command of its own. fy worktree add and
    fy worktree remove read 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 means extensions.partialClone, or any
    remote.<name>.promisor or remote.<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, --force saves. An untracked file counts even when
    status.showUntrackedFiles is no. 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. Set backend = "lima" to move: the next fy up creates a new VM,
    so images are rebuilt there and the podman machine's volumes, agent logins included, don't come
    along.

Changed

  • fy init writes a config that matches what fy does today. Step 2 said to uncomment an agent
    and run fy box up again, which does nothing on a running box: an agent is installed when the
    box is created, so it now says fy config adopt, then fy 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 own gh token as a github-user emergency switch. runtime = "gvisor" is
    marked experimental (a test now holds both scaffolds to the experimental register),
    host_firewall warns that committing it refuses colleagues who aren't on Linux, and keyless
    says it belongs in foldyard.local.toml on a team. In the local example, the private injector
    gets a box_env placeholder, loses a wrong "add it to the allowlist" step and warns that a local
    [[inject]] replaces the shared rows; vmtype, [host] notifications and [claude.settings]
    join it. fy init ends by pointing at fy prereq. Existing foldyard.toml files 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 from claude --resume inside the new box. The first fy box up that
    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_TRANSCRIPTS set, 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. A max_mb in
    ~/.foldyard/<project>/monitor-store.toml still wins (64 MB to 64 GB).
  • The guest monitor names what isn't a container: podman, build and the VM itself. Most
    unattributed events 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 now podman
    and build, and vm also 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 who no longer gets vm and podman events, and every batch now
    counts what the hook's filters passed over, by label (passed), so nothing is left out
    silently. Set skip_unless_asked at the top of monitor-hooks.toml to change the default for
    every hook without who, or name a label in a hook's who to get it. A process that steps out
    of a container into the VM's or podman's namespace is marked crossed, labelled with the
    container it came from, and sent to every hook whatever its who.
  • 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 ...
Read more

foldyard 0.6.2

Choose a tag to compare

@github-actions github-actions released this 22 Oct 13:02

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, while fy monitor hook and fy doctor showed ✓. 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 run fy host restart:
    a hook stuck this way resumes right after the last event it delivered
    , with no duplicates.
    fy monitor hook and fy doctor now warn when a hook's cursor has not moved for ten minutes
    while events kept arriving.

foldyard 0.6.1

Choose a tag to compare

@github-actions github-actions released this 22 Oct 09:50

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 on vz (virtiofs) and
    qemu (9p), so foldyard now pins Lima's mountType to 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 -fdx and grep no longer see them and fy worktree remove archives the slot, then
    deletes it. For this release the transcript sync reads both places; the next drops the old one.
    fy verify checks the to-box with a real write, and fy doctor has a to-box row.
    Every Lima VM needs one fy 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 up
    to move a box's transcripts out of the checkout (fy box up warns
    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_DIR must now point outside the checkout and the worktrees root on Lima
    (ADR-0033).

Added

  • fy monitor hook add NAME --to-box puts 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
    with fy monitor check. It needs the lima backend on vz or qemu; elsewhere it says why and
    points at --file, which works as before. A --file inside 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 gaps list (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 same fy.monitor.events/1 schema;
    a batch can now hold gaps alone (events empty, first/last null), and file-sink lines
    that carry them still pass fy monitor check.
  • Experimental: fy monitor --since/--until reads 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. Set max_mb in ~/.foldyard/<project>/monitor-store.toml (64 MB to 64 GB);
    it is never read from foldyard.toml, so the box can't shorten what is kept about it.
    fy monitor and fy doctor show how much is kept and how far back. Old files are converted in
    place; anything copying logs/monitor*.jsonl should now also take monitor*.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 labelled unattributed, a who = ["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 what fy monitor over the whole store
    shows. Events dropped before this fix can't be recovered: the store had already removed them.
    fy monitor had the same flaw and is fixed too. Restart the host supervisor
    (fy host restart) to pick it up.

foldyard 0.6.0

Choose a tag to compare

@github-actions github-actions released this 21 Oct 23:11

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 make fy box down,
    fy nuke, fy worktree remove, fy transcripts or the transcript_sync_seconds sweep 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 for transcript_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_seconds sweep) could place box-written sessions where claude --resume in 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 by fy transcripts and 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/sessions at all (below).
  • Box Codex sessions are no longer archived into ~/.codex/sessions on 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 resume when you worked in that project. fy transcripts, fy box down, fy nuke,
    fy worktree remove and the transcript_sync_seconds sweep 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 resume from the checkout after
    logging in there once; fy transcripts prints the line. An explicit archive dir
    (fy transcripts <dir> or FOLDYARD_TRANSCRIPTS_ARCHIVE) still files them under
    codex-sessions/. Sessions earlier versions archived into ~/.codex/sessions stay where they
    are — foldyard never moves or deletes them; each also sits under the same name in the
    checkout's .devbox-codex/sessions unless 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 (authorization on
    api.anthropic.com) and an [[inject]] row writing x-api-key there, with a box_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 mode refused 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
    it no 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's path_prefix (marked placeholder out of scope). Two injectors
    writing the same header, or one whose row has no box_env, are still refused. Give the row's placeholder
    a variable only the harness reads: Claude Code prefers ANTHROPIC_API_KEY over its OAuth token.
    See fy docs configuration.
  • fy worktree add suggests a directory name when given a branch-shaped one. The first
    argument is the worktree's directory, so fy worktree add sec/vuln-bumps is refused (a nested
    directory would never be discovered). The refusal now suggests
    fy worktree add sec-vuln-bumps sec/vuln-bumps instead of leaving you to work out which
    argument was wrong.
  • fy claude starts 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, which fy claude always 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 on auto, which follows your terminal's light or dark background.
    All three are defaults: a theme you pick with /theme stays.
  • The VM's boot scripts now say "VM firewall" and "host" instead of "wall" and "Mac". Their
    comments, messages, the guest's fy-wall unit 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 (its uninstall
    runs then). So every Lima VM, with or without [machine] firewall, is refused as stale once:
    fy up stops until you run fy machine stop && fy up.

Added

  • The docs and both bundled skills explain why a cache prune can fail with Permission denied in 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, run fy skill install --force foldyard and
    fy skill install --force bootstrap-devbox.
  • Experimental: fy monitor shows 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's monitor events row). 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.toml on 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, so host.env secrets 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 --json output 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 doctor shows
    whether it is observing. Off by default; changing it needs fy machine stop && fy up
    (ADR-0032).
  • Five new pages in fy docs: workshop, stack, git, github-app and gcp. 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...
Read more

foldyard 0.5.0

Choose a tag to compare

@github-actions github-actions released this 19 Oct 11:31

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, fy says so instead of inventing one. With no foldyard.toml
    in the current directory or above (and no FOLDYARD_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 at fy init and fy prereq, exits 1 and writes nothing. That includes a git
    repo that has no foldyard.toml.
  • Help, fy prereq, fy docs, fy init, fy machine base and fy skill list work anywhere,
    quietly.
    Every verb, --help included, used to open with "✗ not a git repository" outside a
    git repo, including uvx foldyard prereq in 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:port for
    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 carry method, path and mismatch to 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 at CONNECT,
    before TLS, so the row could only say CONNECT host:443. It now answers the CONNECT itself,
    decrypts the tunnel and returns the 403 to 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 :443 and an image
    build are still refused at CONNECT. 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 readable 403. The log still records it as a blocked CONNECT.

Added

  • fy prereq checks what your computer needs and prints how to install it. Lima (and its
    version), podman, a hypervisor, and on Linux QEMU, /dev/kvm and a systemd --user session,
    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 prereq works 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 (or fy 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 up create a new project's VM as a limactl clone of it with the
    project's own sizing and mounts, and fy box up re-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=0 turns cloning off. fy docs base-vm has the rules.
  • Node's fetch() and npm's install scripts go through the proxy without project wiring.
    Node's fetch() ignores HTTPS_PROXY unless NODE_USE_ENV_PROXY is set, so under the VM
    firewall it failed with an ECONNREFUSED against 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 gets NODE_USE_ENV_PROXY=1, npm_config_https_proxy
    and npm_config_proxy (defaults: [box].env may 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 a NO_PROXY
    naming the stack's services (fy docs networking, "Node and npm"). The stack containers'
    settings are part of the VM's boot setup, so fy up refuses a running VM until you run
    fy machine stop && fy up once.
  • fy doctor points 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 new stack env overrides row 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 own ENV (an SSL_CERT_FILE of 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, then fy 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 build fetching Google Fonts ignores NODE_EXTRA_CA_CERTS;
    NEXT_TURBOPACK_EXPERIMENTAL_USE_SYSTEM_TLS_CERTS=1 makes it read SSL_CERT_FILE.
  • [box].packages installs 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 by fy 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].image too. A box that is already running keeps its old image until
    fy box down && fy box up.
  • fy changelog prints what changed between the foldyard your foldyard.toml declares and the
    one installed.
    A project can sit any number of releases behind the fy running it, and
    catching its config up meant reading the whole changelog on GitHub. Now the releases in between
    (after min_foldyard_version, or recommended_foldyard_version if higher, up to the installed
    version) print oldest first, from the changelog the install itself ships, so it works offline
    and inside the box. --action keeps only what can need a step (each release's summary plus
    Security, Removed, Deprecated and Changed); --since/--until pick the range by hand and work
    outside a project. Raise min_foldyard_version once 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 date foldyard.toml was 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).
    --since also 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
    git ran 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 logs works on the podman engine. It asked compose for the last 50 lines with -n 50,
    which docker compose reads as --tail 50 but the bundled podman-compose reads as its
    value-less --names switch followed by a service called 50, so every fy 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 up and fy box build wait for the host supervisor's daemons, and refuse
    when one ...
Read more

foldyard 0.4.0

Choose a tag to compare

@github-actions github-actions released this 08 Oct 14:32

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 by fy box up;
    until then a stale host-side git status is fixed by git 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 user fy-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 up there)
    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, and fy up refuses the old
    table until it's replaced.
    The refusal prints the rows to paste, computed from your table: a
    kind = "github-app" row (your app_id, installation_id, repo as repositories, and
    box_env = { GH_TOKEN = "x" }) and, commented out, a kind = "gh-cli" emergency row for the old
    user level. Three things move with it. The App's private key is read from the switch's
    FY_INJECT_<SWITCH> in host.env (FY_INJECT_GITHUB for switch = "github") — rename
    GH_PEM_B64, same value, and any [[secret]] row naming it. fy mode github=app is
    fy mode github=on, and the old github=user is the gh-cli row's own switch. And
    permissions has no successor: the token now carries the App installation's own
    permissions
    — foldyard no longer narrows it to pull_requests/issues or refuses a map that
    widens it, which is what turned a consumer's granted actions: read into 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 installs gh in the box: put it in your box image.

Changed

  • uv tool install foldyard is 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 by fy box up with 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 exact fy mode command to run first. Before, a consumer's
    [[inject]] row on api.anthropic.com beside keyless claude=on both 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 in fy mode and
    fy state, a credential overlap row in fy doctor, and both rows marked HELD BACK in
    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 until fy box down && fy box up. The box now gets the route and its
    box_env placeholders whenever a credential is declared, and the proxy listener runs for it.
  • The proxy now decrypts the toolchain too: [proxy] passthrough defaults 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, so npm, 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) and apt over HTTPS (@linux) stay tunnelled. A
    project that sets passthrough itself 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 next fy 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 sets CLOUDSDK_CORE_CUSTOM_CA_CERTS_FILE, so
    gsutil and bq trust the proxy like gcloud does.
  • [box].env is your project's environment, and refuses the names foldyard sets in the box.
    fy box up now stops before starting anything when [box].env sets 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 as PATH. 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). TZ and LC_TIME stay yours to
    set. A key that isn't a plain environment-variable name is refused too: a quoted
    "HTTPS_PROXY=x" would otherwise have set HTTPS_PROXY. An [[inject]] row's box_env
    already refused the same names.

Added

  • fy doctor in 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 on PATH, or a shim whose hooks didn't install, used to switch all of that off with
    no sign. Two new rows say so. git shim fails when git isn't the shim, and git 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 runs fy doctor
    once when it's created, as the last lines of fy box up, so it reports this without being
    asked. (ADR-0021.) Its egress proxy CA row now says the box must be recreated too: the CA
    is mounted when a box is created, so fy host restart alone never reached an existing box.
  • foldyard shows what a GitHub App installation grants, and tells you when that changes.
    foldyard no longer caps a github-app token'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 widenings shows the last reading under the row and how old it is, flagging any
    write or admin permission, and any level foldyard doesn't recognise as if it were one
    ("not yet probed" until the switch has been on), and fy mode and fy tui show 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,
    naming installation_id.
  • With an [[inject]] switch off, a request carrying its box_env dummy gets an answer naming
    the fix instead of the provider's "Bad credentials".
    The proxy answers it itself with a 401
    whose JSON message says the switch is off and to run fy mode <switch>=on on your computer,
    which gh and most clients print. It matches where the row's rule would inject (host,
    path_prefix, header or query_param), and in a header the dummy may stand alone or follow one
    auth scheme (`g...
Read more