Skip to content
neblerPublic

About

No description, website, or topics provided.

Resources

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

Fern

ci OpenSSF Scorecard Go 1.27 gVisor MIT

Give every AI coding thread its own environment that behaves like git for everything: code, dependencies and the running database.

Every turn is a snapshot; revert brings back the code and the database

Fern runs behind T3 Code. Each thread gets a sandboxed box with its own copy of the repository and its services (such as Postgres or a Vite dev server). Every agent turn is snapshotted, so reverting a thread in T3 brings back the files and the database as they were. Idle boxes sleep and wake on the next message, or when someone opens the thread's preview. Claude runs as the real claude CLI on your own subscription: no API keys.

What a thread gets:

  • Undo for everything. A btrfs snapshot per turn (12 ms); a revert puts back files and Postgres together (about 0.1 s).
  • A head start. A new thread starts from the repository's golden snapshot in 35 ms, with its pinned tools (Node, pnpm, … through mise), node_modules and its initialised database.
  • Web development. Dev servers Claude starts keep running; they hear Claude's edits; http://<thread>.localhost:30780/ is a preview that wakes the box and reloads by itself (edit to page: 0.4 s).
  • Sleep when idle. 30 s after a turn, Claude is stopped and both boxes sleep: no CPU until the next message.

How it works

flowchart LR
    app["T3 app<br/>Mac or phone"] -->|pairing link| t3

    subgraph server["One Linux server"]
        t3["T3 server<br/>fern-t3.service"]

        subgraph daemon["fern-daemon.service"]
            api["API<br/>unix socket"] --> actors["actors<br/>one per environment"]
            actors --> store["store<br/>btrfs"]
            actors --> runsc["box driver<br/>runsc"]
            mcp["MCP Bash"]
        end

        t3 -->|"run Claude, checkpoint,<br/>revert"| api
        runsc --> agent["agent box<br/>Claude"]
        runsc --> box["environment box<br/>fern-init, Postgres, app"]
        agent -->|Bash tool| mcp
        mcp -->|exec| box
        store --- pool[("btrfs pool<br/>workspaces, snapshots")]
    end
Loading
  • The daemon (fern-daemon) owns the environments. Each one has an actor, a goroutine that makes its changes one at a time, counts the commands running in its boxes and puts them to sleep when idle.
  • The store keeps each workspace as a btrfs subvolume. A snapshot is a btrfs snapshot (milliseconds, no copy), and a revert swaps the workspace for a copy of one in a single atomic rename. The disk is the only record: an environment is its folder with a small record file in it, and there is no database beside them.
  • Boxes are gVisor sandboxes the daemon starts itself with runsc, each with its own network namespace, an nftables firewall (internet only) and cgroup limits. Claude runs in an agent box of its own; its shell commands go through the daemon to the environment box, where the services are. The environment box never sees Claude's login.
  • Golden snapshots hold a repository's built setup (its pinned tools, installed dependencies, an initialised database), so a new thread starts from one instead of running setup again.
  • The front door (:30780) serves each box's ports as previews by host name, waking an asleep box; a request in progress, such as a live reload's WebSocket, keeps it awake. A fanotify watch on the host passes Claude's edits on to the dev servers in the environment's box, which gVisor would otherwise hide from them.

A turn, end to end:

sequenceDiagram
    participant T3 as T3 server
    participant D as fern-daemon
    participant A as agent box (Claude)
    participant B as environment box

    T3->>D: run Claude in the thread's worktree
    D->>A: wake the box, start Claude
    A->>D: Bash: run the tests
    D->>B: wake on first use, run the command
    B-->>D: output
    D-->>A: output
    T3->>D: checkpoint the turn
    D->>D: btrfs snapshot of the workspace
    Note over D,B: nothing to do for 30 s: Claude is stopped, both boxes sleep
    T3->>D: revert to an earlier turn
    D->>D: stop the boxes, swap the workspace for the snapshot
    Note over T3,B: the next message wakes the boxes and resumes Claude's session
Loading

Benchmarks

Snapshot 12 ms, revert 86 ms, wake 220 ms, new thread 35 ms

50 MiB per awake box, 20 awake boxes tested at once, no CPU while a thread sleeps, 11 MiB for the daemon

On a Hetzner CAX21 VPS (4 ARM cores, 8 GB), gVisor, medians:

time CPU
snapshot a turn 12 ms 20 ms
revert (files and database) 86 ms 140 ms
wake a box 220 ms 330 ms
put a box to sleep 71 ms 130 ms
run a command in an awake box 19 ms
new thread from a golden snapshot 35 ms
three forks of a running thread, Postgres included 77 ms
Claude's edit to a reloaded preview 416 ms
a whole short agent turn 7.3 s

Memory: 16 MiB for an awake box with Postgres idle, 10.8 MiB for the daemon. An idle box sleeps 30 s after a turn and then uses no CPU. An asleep thread costs about 75 KiB of disk and no memory: 585 of them added 0.9 MiB to the daemon, and a wake among them still takes 231 ms.

Against the first design (agent-sandbox pods on Kubernetes): 8.7× less CPU per turn, revert 9.6× faster, 20 boxes woken at once 25 s → 1.2 s.

A heavy dev server's first page: TanStack Start renders it in about 10 s in a box. Fern renders it ahead when Claude's turn wakes the box, so the page comes in 0.11 s; a browser that wakes an asleep thread itself still waits 16.6 s (4.6 s under runc: gVisor without KVM costs about twice the CPU).

How each number was measured: lab/RESULTS.md.

Engineering

  • Crash-safe by construction. Every multi-step change leaves the disk showing how far it got (a marker file, a copy beside the workspace, a folder without its record), and Reconcile finishes or clears it on start. Crash tests stop each change at every one of its steps, on a real btrfs filesystem in CI.
  • Actors, tested with a fake clock. One goroutine per environment, talked to through its mailbox. Idle sleeps, pre-wakes and concurrent changes are tested in testing/synctest bubbles, with goleak checking for leaked goroutines.
  • Performance is gated. lab/perf.sh fails a change that makes any of its 288 metrics worse than the baseline (time, CPU, memory, disk and network). lab/ab.sh runs two versions in alternation on the same machine and gives each metric's change with a 95% interval and a p-value. lab/profile.sh traces an operation with OpenTelemetry and draws flamegraphs of the whole node.
  • Sandboxing in depth. gVisor, no capabilities, a read-only root, the repository's git folder mounted read-only, a firewall that allows only the internet, and the login kept out of every box that runs the repository's code. Threat model: docs/briefs/claude-outside-the-box.md; reporting: SECURITY.md.
  • Checked on every pull request. Unit, btrfs and fuzz tests; golangci-lint, shellcheck and hadolint; govulncheck on the Go and Trivy on the images; generated code and go.mod up to date. Actions are pinned to commits, and OpenSSF Scorecard grades the repository weekly.

Try it

You need a Linux server for Fern alone (Ubuntu 26.04 or Debian 13, arm64 or amd64, 4 GB or more; others may work) that you reach as root with an SSH key, a Claude subscription and the T3 app. From your Mac:

curl -fsSL https://raw.githubusercontent.com/nebler/fern/main/install.sh | bash -s -- root@1.2.3.4

It installs Fern and T3's server on the server from the released images (a few minutes the first time, about a minute for an update), asks once for your Claude login there (claude setup-token: open the link, paste back the code, then the token), keeps an SSH tunnel to the server running in the background and copies a pairing link. In the T3 app: Settings → Connections, paste the link. Then open the sample project and start a thread: ask Claude to drop the email column, then revert.

Run the same command again to update, or for a new pairing link. When something doesn't work, bash install.sh doctor root@1.2.3.4 checks the server and the tunnel and says what to do. What to do for each failure, how to roll back an update, and what to set up so you hear of problems: docs/operations.md. The server gets a firewall that lets in only SSH (FERN_FIREWALL=off keeps yours): T3's port is never public, and the tunnel is how your Mac reaches it and the previews (http://<thread>.localhost:30780/).

Private repositories, push and pull requests: T3 pushes and opens pull requests from its server, with tokens from a GitHub App of your own: each lasts an hour and is for one repository, and none ever enters a box. Once:

  1. Create the App at github.com/settings/apps/new: any name and homepage; under Webhook, untick Active; under Repository permissions, Contents and Pull requests: Read and write (and for deploys to Cloudflare from a thread, Workflows and Secrets: Read and write too). Create it, note its App ID, and under Private keys, generate one (a .pem downloads).
  2. Install it (Install App, in its settings) on the repositories Fern may use.
  3. Run the install again with it: FERN_GITHUB_APP_ID=123456 FERN_GITHUB_APP_KEY=~/Downloads/your-app.pem bash install.sh root@1.2.3.4 (or through curl … | FERN_GITHUB_APP_ID=… FERN_GITHUB_APP_KEY=… bash -s -- root@1.2.3.4).

Cloudflare: a repository that deploys to Cloudflare (wrangler, or Astro's, Vite's or OpenNext's Cloudflare adapter) runs in its box with D1, R2 and KV emulated locally, and their data is reverted with each turn. Claude gets Cloudflare's own skills (cloudflare/skills) and Fern's fern-deploy there. To deploy from a thread, give the server a Cloudflare API token once: run the install again with FERN_CLOUDFLARE_TOKEN_FILE=~/cloudflare-token (create it from this pre-filled form; in the form, also add User → User Details → Read, which Cloudflare's MCP server needs to accept the token), and give your GitHub App Workflows and Secrets. Then ask Claude in a thread to put the site online: it adds a GitHub Actions workflow and has Fern set the repository's secret (Claude never sees the token), and every push deploys, main live and other branches as preview URLs. With the token, Claude also gets Cloudflare's MCP server, in its own box only, to create D1 databases and R2 buckets or read a deploy's logs. Delete /etc/fern/cloudflare.env on the server to stop.

Claude's skills: Claude gets skill packs, each a set of skills baked into the box image (pinned) that it uses on its own:

Pack What Claude gets On
cloudflare Cloudflare's skills and Fern's fern-deploy in repositories that deploy to Cloudflare
safety Fern's site-safety (keys, visitors' input, uploads, bots, data) and Cloudflare's turnstile-spin; /security-review before the first deploy by default
beginner plain words, the preview link after every visible change, errors fixed rather than shown opt-in
design Emil Kowalski's design skills: emil-design-eng, animate, mobile-native opt-in
engineering Dillon Mulroy's coding-standards and anti-slop linter, Matt Pocock's codebase-design and diagnosing-bugs opt-in

Choose a server's once: FERN_SKILLS="beginner design engineering safety" with the install (or install.sh skills "…" on the server); every thread on the server gets them from its next wake. An Astro site's Claude also gets Astro's docs server, so it reads current docs.

Claude's pause: Fern stops a thread's Claude after 30 seconds with nothing to do, so its box can sleep, and starts it again at the next message, which then takes about 3 seconds longer. To keep it for the messages that follow, at about 160 MiB a thread meanwhile, give T3's service a longer one on the server: Environment=FERN_HIBERNATE=5m under [Service] in /etc/systemd/system/fern-t3.service.d/hibernate.conf, then systemctl daemon-reload && systemctl restart fern-t3.

Backups: hourly to an S3 bucket (Hetzner Object Storage, R2, any S3), encrypted before they leave the server with a key that stays on your Mac (~/.config/fern/backup-key; keep a copy). Each thread's workspace goes as a btrfs send stream, incremental after the first, with each thread's record, T3's data and the projects' repositories. Create a bucket and an access key, then run the install again with FERN_BACKUP_ENDPOINT=https://fsn1.your-objectstorage.com FERN_BACKUP_REGION=fsn1 FERN_BACKUP_BUCKET=… FERN_BACKUP_ACCESS_KEY=… FERN_BACKUP_SECRET_KEY=…. To bring a lost server's threads onto a new one, install the new one with the same settings and FERN_RESTORE_FROM=<old server's hostname>.

Working on Fern itself: lab/setup.sh builds everything from your working tree, in a local lab (a systemd Linux node in Docker) or on a VPS with FERN_HOST=1.2.3.4; it needs Docker for the local lab and bash 5 (brew install bash). After a change, lab/dev.sh runs the unit tests, builds and runs the quick integration checks. AGENTS.md lists every command.

Repository

cmd/fern-daemon the daemon: wiring only
cmd/fern-init a box's main process: runs the services, serves exec
cmd/fern the CLI
internal/actor one actor per environment, idle sleep, golden builds
internal/store the btrfs pool, snapshots, retention, Reconcile
internal/runsc boxes as gVisor sandboxes: bundle, network, firewall
internal/boxexec the exec protocol over WebSocket
internal/hibernate stops an idle Claude and resumes it unseen
internal/setup the setup contract and the golden key, pinned tools included
internal/preview the front door: previews by host name, waking boxes
internal/watch passes Claude's edits on to the dev servers
internal/api the daemon's HTTP API on a unix socket, and its client
lab the lab, integration checks, perf gate, A/B and profiler
docs architecture, design, roadmap and decision briefs

Status

Built and measured on a VPS: environments, snapshots per turn, revert, golden snapshots, idle sleep and wake, pre-wake, agent boxes, the T3 integration (T3 patches in images/t3), web development in a thread (dev servers, previews and pinned tools), your own repositories without Fern files, GitHub access through your GitHub App, fern doctor, supervision of crashing boxes, encrypted backups, rendering the first page ahead, forks (Claude's env_fork, env_exec, env_delete) and a one-command install from released images.

Next (roadmap): Codex next to Claude.

Fern v1, a control plane for coding-agent jobs in disposable containers, is at tag v1-final.

License

MIT

About

No description, website, or topics provided.

Resources

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages