- Shell 54.7%
- Nix 45.3%
| .crushrc | ||
| .envrc | ||
| .gitignore | ||
| AGENTS.md | ||
| crushbox | ||
| flake.lock | ||
| flake.nix | ||
| plan.md | ||
| README.md | ||
crushbox
Run Crush coding agents on Forgejo issues inside ephemeral VMs, built with microvm.nix.
Given an issue reference, the crushbox script:
- fetches the issue title and body from the Forgejo API,
- creates a git worktree of your local checkout on a branch
crushbox/<task>, - builds and boots an ephemeral qemu MicroVM that has the worktree (and the
checkout's
.git, needed by the worktree) mounted at the same paths as on the host, - runs
crush runinside the project'snix developenvironment with a prompt describing the issue, and - lets the VM power itself off when the agent is done, then prints the resulting commits.
With -p/--prompt, steps 1 and 4 use your prompt text directly instead of a
fetched issue, and the task is named prompt-<slug>.
Worker permissions are allow/deny only, never ask (there is nobody to answer inside the VM). The guest merges a crush.json into the worker's Crush config that
- allows every built-in tool (
permissions.allowed_tools), - hides the interactive
questiontool (options.disabled_tools), and - installs a catch-all
PreToolUsehook returning{"decision": "allow"}for anything not on the list.
Inspired by Coding Agent VMs on NixOS with microvm.nix.
Requirements
- NixOS host with flakes enabled; KVM recommended (
/dev/kvm), without it the VM falls back to slow TCG emulation git,curl,jq- No root, bridge, or TAP setup needed: the VM uses qemu user-mode networking (outbound NAT only), and shares are served by a per-task virtiofsd running as the calling user
Setup
Create ~/.config/crushbox/config:
CRUSHBOX_FORGEJO_URL=https://git.example.com
CRUSHBOX_PROJECT=owner/repo
CRUSHBOX_REPO_DIR=$HOME/src/repo # optional; defaults to the current directory
CRUSHBOX_TOKEN=... # optional, for private issues
See crushbox --help for all settings.
The agent authenticates to its LLM provider via API key environment variables
(see CRUSHBOX_PASS_ENV) and/or your crush config directory, which is copied
into the task directory and into the VM (~/.config/crush).
Usage
crushbox 42 # issue 42 of the configured project
crushbox owner/repo#42 # explicit project
crushbox https://git.example.com/o/r/issues/42
crushbox -p "refactor the parser to use iterators" # ad-hoc prompt, no issue
crushbox list # task overview
crushbox clean repo-42 # remove worktree and task files
While a task runs, follow the VM console or the agent output:
tail -f ~/.local/state/crushbox/tasks/repo-42/vm/console.log
tail -f ~/.local/state/crushbox/tasks/repo-42/crush-output.log
When it finishes, review and merge on the host:
git -C ~/src/repo log -p crushbox/repo-42
git -C ~/src/repo merge crushbox/repo-42
How it works
flake.nixdefines anixosConfigurations.crushbox-workerMicroVM (qemu) containing crush, git and nix. Share sources, the task id and the host user's uid/gid are passed via environment variables (builtins.getEnv), so builds use--impure. The task id reaches the guest as acrushbox.task=<id>kernel parameter.- The host checkout (
CRUSHBOX_REPO_DIR) and the state directory are virtiofs shares mounted at identical absolute paths inside the guest, so the worktree's.gitfile resolves correctly and commits land in the real repository. The script starts one virtiofsd per share as the calling user, so no root is needed and file ownership matches on both sides. - A oneshot systemd service in the guest reads the task directory, sources
the env file, and runs
nix develop --command crush run "<prompt>"(or plaincrush runif the project has noflake.nix), records the exit code inexit-codeand lets a root service power off the VM (also on failure, dumping the boot journal tojournal.logfirst). - The guest runs the task as a
workeruser with the host user's uid/gid. - VM state (
var.img) lives in the per-task directory and is removed bycrushbox clean(thecrushbox/<task>branch is kept).
Limitations / notes
- The agent has write access to the whole checkout at
CRUSHBOX_REPO_DIR(worktree commits require access to the common git dir). It is instructed to only touch its worktree, but this is convention, not isolation. Everything else on the host is unreachable from the VM. - When
CRUSHBOX_REPO_DIRis unset it defaults to the current directory and the task state moves to./.crushbox(automatically added to.git/info/exclude), so nothing outside the current directory is writable in the VM. With an explicitCRUSHBOX_REPO_DIR, the state directory (~/.local/state/crushboxby default) is a second writable share. - Dev shell downloads inside the VM land in a tmpfs overlay (
/nix/.rw-store), sized bymicrovm.mem(8 GB default). Large dev environments may need more. - The permission allow-list names tools as of crush 0.88.0; the catch-all
PreToolUsehook covers the top-level agent, but tools invoked by sub-agents bypass hooks, so a tool missing fromallowed_toolsin a future crush version could stall a sub-agent untilCRUSHBOX_TIMEOUTfires. crush runhas no--yoloflag (it is TUI-only), which is exactly why the guest enforces allow/deny through configuration instead.