Documentation
¶
Overview ¶
Firecracker provider (Linux). Boots an OCI image as an ext4 rootfs with clawk-init as PID 1 and the clawk-pty-agent on vsock — the same guest stack as the vz provider — and talks to it over firecracker's hybrid vsock. No sshd: every guest session goes through the agent, like vz.
Networking mirrors vz too: the VM runs out of process under the __fcd daemon, which drives an in-process gvproxy (gvisor-tap-vsock) userspace TCP/IP stack as the guest's gateway and enforces the same per-connection egress allow-list + DNS-aware filtering (internal/netfilter.AllowList). gvproxy can't drive firecracker's TAP directly, so the daemon bridges the two with a frame pump (fcnet_linux.go).
Known limitations:
- One shared bridge / one fixed guest IP, so two firecracker sandboxes at once collide. A per-sandbox /30 is the next step.
- No virtio-fs, so the phase worktree is baked into the rootfs at Create time rather than live-mounted; host edits don't propagate.
Linux VM provider plumbing — bridge + TAP, firecracker-ci kernel fetch, loop-mounted rootfs. Consumed by the firecracker provider; kept separate because the asset/network plumbing is sizeable and unrelated to the hypervisor-specific spec building in firecracker_linux.go.
Index ¶
- Constants
- func CheckGuestABI(sb *config.Sandbox) error
- func ClearOAuthToken(clawkRootDir string) error
- func ConsoleTail(path string, n int) string
- func GuestWorkspace(p Provider) string
- func HasManagedWorktree(sb *config.Sandbox) bool
- func InPlaceWorktreeTag(worktreePath string) string
- func InitRootHelpers()
- func OAuthTokenPath(clawkRootDir string) string
- func OCIGuestManifest(sb *config.Sandbox, stateDir, cacheDir, rootDir string) (guestcfg.Manifest, error)
- func OCIRootFS(sb *config.Sandbox, cacheDir string, bins guestbuild.Binaries) machine.OCIImage
- func RecordedGuestABI(sb *config.Sandbox) int
- func RepoShareTag(repoPath string) string
- func SaveOAuthToken(clawkRootDir, token string) error
- func SeedClaudeMemory(stateRoot, seed string) error
- func SeedClaudeStateDir(stateRoot, clawkRootDir string) error
- func WriteOCIGuestConfig(sb *config.Sandbox, vmDir, stateDir, cacheDir, rootDir string) error
- type Bar
- type CaptureExecProvider
- type ExitError
- type FirecrackerProvider
- func (f *FirecrackerProvider) Create(sb *config.Sandbox) error
- func (f *FirecrackerProvider) DaemonSpec(sb *config.Sandbox, allow *netfilter.AllowList) (machine.Spec, error)
- func (f *FirecrackerProvider) Destroy(sb *config.Sandbox) error
- func (f *FirecrackerProvider) Exec(sb *config.Sandbox, command ...string) error
- func (f *FirecrackerProvider) ExecCapture(sb *config.Sandbox, command ...string) (string, error)
- func (f *FirecrackerProvider) FCStateDir(sb *config.Sandbox) string
- func (f *FirecrackerProvider) GuestWorkspaceRoot() string
- func (f *FirecrackerProvider) Shell(sb *config.Sandbox, workdir string) error
- func (f *FirecrackerProvider) Start(sb *config.Sandbox) error
- func (f *FirecrackerProvider) Status(sb *config.Sandbox) (string, error)
- func (f *FirecrackerProvider) Stop(sb *config.Sandbox) error
- type HostFile
- type HostShare
- type MockProvider
- func (m *MockProvider) Create(sb *config.Sandbox) error
- func (m *MockProvider) Destroy(sb *config.Sandbox) error
- func (m *MockProvider) Exec(sb *config.Sandbox, command ...string) error
- func (m *MockProvider) Shell(sb *config.Sandbox, workdir string) error
- func (m *MockProvider) Start(sb *config.Sandbox) error
- func (m *MockProvider) Status(sb *config.Sandbox) (string, error)
- func (m *MockProvider) Stop(sb *config.Sandbox) error
- type OAuthTokenSource
- type PlainProgress
- type Progress
- type Provider
- type RootExecProvider
- type ShellProvider
- type WorkspaceProvider
Constants ¶
const CurrentGuestABI = 1
CurrentGuestABI is the version of the guest-side contract baked into a sandbox's disk at create time. Two things move together under this one number: the clawk-init boot manifest schema (guestcfg.Version) and the pty-agent vsock protocol (vsockproto.ProtoVersion). It is recorded on the sandbox record (config.Sandbox.GuestABI) so a later host binary can tell — without booting anything — whether it still speaks this sandbox's dialect.
const DefaultDiskSizeGiB = 8
DefaultDiskSizeGiB is the virtual ceiling every VM root disk is grown to. The image is sparse, so this bounds runaway growth rather than charging steady-state cost — a box only ever occupies the blocks it has actually written. Build caches live on shared virtio-fs mounts (see ToolchainCacheShares), which keeps each rootfs's working set small.
OCI rootfs disks are padded sparse ext4 images sized to this ceiling at build time.
const GuestHome = "/home/" + GuestUser
GuestHome is the agent user's home dir. Kept as a constant (not `/home/` + GuestUser expression) so it can be concatenated at compile time in other constants.
const GuestUser = "agent"
GuestUser is the non-root Linux user the agent runs as inside every sandbox. "agent" mirrors Docker AI Sandbox's convention (docker/sandbox-templates:claude-code, UID 1000, /home/agent), which keeps our docs and the wider ecosystem's docs interchangeable.
All user-facing paths inside the VM are derived from this constant (GuestHome, WorkspaceRoot, ~/.claude/... mounts, etc.), so if we ever switch the base image's user, nothing else has to change.
const MinSupportedGuestABI = 1
MinSupportedGuestABI is the oldest recorded guest ABI this host binary still boots and attaches to. Raising it turns every sandbox created before the new floor into a "recreate me" error, so raise it only alongside an actual manifest/protocol break — and say so in the changelog.
const NinepBasePort uint32 = 1100
NinepBasePort is the first guest vsock port used by the host 9p cache servers; each ToolchainCacheShares entry gets NinepBasePort+index. Chosen clear of the fixed control ports (1024 pty-agent, 1025 time-sync, 1026 ssh-agent) with headroom so adding a cache never collides.
const OCICmdline = "console=hvc0 root=/dev/vda rw psi=1 init=" + guestcfg.InitPath +
" clawk.cfg=/dev/vdb"
OCICmdline boots the image's flattened rootfs with the injected init. hvc0 is the vz virtio-console; root is the OCI-built ext4 on the first virtio-blk; clawk.cfg names the manifest config disk. psi=1 enables the kernel's pressure-stall information (CONFIG_PSI_DEFAULT_DISABLED is set in the Kata kernel) so the guest agent can report /proc/pressure/memory to the host balloon controller.
const OCIConfigDiskName = "guestcfg.img"
OCIConfigDiskName is the manifest disk's filename inside a sandbox's VM directory.
const WorkspaceRoot = GuestHome + "/workspace"
WorkspaceRoot is where worktrees get mounted inside the guest. Lives under the agent user's home (same convention as Docker AI Sandbox), not at the filesystem root — many agent tools (e.g., the Claude Code skills installer) create dotfiles next to the current working directory, and $HOME is writable by default. A root-owned /workspace causes "EACCES mkdir /workspace/.agents" errors.
WorkspaceShareTag is the virtio-fs tag for the single consolidated worktree share: the host worktree parent (store.WorktreeDir) mounted at WorkspaceRoot. Every managed (non-in-place) worktree lives under that one host dir, so folding them into a single virtio-fs device — instead of one device per phase — keeps sandboxes with many repos under the device ceiling Apple's Virtualization.framework enforces (see machine/vz). Both the vz share list (collectSandboxShares) and the guest mount manifest (OCIGuestManifest) key off this exact tag, so they must never diverge.
Variables ¶
This section is empty.
Functions ¶
func CheckGuestABI ¶
CheckGuestABI refuses, with recreate guidance, when the guest binaries baked into sb's disk are outside what this host supports. Called before boot and before attach: it is the readable version of the failure the guest itself would otherwise produce mid-boot (clawk-init manifest check) or mid-attach (pty-agent handshake check).
func ClearOAuthToken ¶
ClearOAuthToken removes the persisted token. Removing a missing file is not an error — `clawk auth clear` should be idempotent so users can run it after losing track of state.
func ConsoleTail ¶
ConsoleTail returns the last n non-empty lines of a guest console log, framed for embedding in a boot-failure message. Returns "" when the log is missing or empty — callers append it to their error text unconditionally.
Boot failures are diagnosed from the guest console essentially every time (kernel panics, clawk-init errors, missing init binaries); making the CLI surface it directly turns "go read this file" into an answer.
func GuestWorkspace ¶
GuestWorkspace returns the per-provider guest workspace path, falling back to the package default when the provider does not implement WorkspaceProvider.
func HasManagedWorktree ¶
OCIGuestManifest assembles the clawk-init boot manifest for sb: the guest-side description of the user, network, mounts, and snapshot files clawk-init applies at boot. stateDir is the per-sandbox persistent state dir (Claude state), cacheDir the clawk cache (toolchain shares), rootDir the clawk root (~/.clawk).
Mount tags MUST stay in lock-step with the share enumeration the vz daemon exposes (collectSandboxShares in internal/cli/vzd.go) — a tag listed here but not exported by vz fails to mount in the guest, and vice versa silently hides a share. HasManagedWorktree reports whether sb has at least one non-in-place phase worktree — i.e. a worktree clawk created under store.WorktreeDir rather than pointing at the user's own repo. Only then is the consolidated WorkspaceShareTag mount emitted (an in-place-only sandbox keeps WorkspaceRoot a plain guest dir with per-phase sub-mounts). Shared by the spec side (collectSandboxShares) and the manifest side (OCIGuestManifest) so both agree on whether the parent device exists.
func InPlaceWorktreeTag ¶
InPlaceWorktreeTag is the virtio-fs tag for an in-place worktree device (from `clawk here`, where the worktree is the user's own directory). Keyed on the full worktree path. Note the guest mount POINT keeps the readable basename; only the opaque tag is bounded.
func InitRootHelpers ¶
func InitRootHelpers()
InitRootHelpers handles privileged mount/umount re-exec before cobra runs. Returns only if os.Args does not match a helper command; otherwise it exits the process.
func OAuthTokenPath ¶
OAuthTokenPath returns the on-disk location of the persisted token. Public so the CLI can echo it back in `clawk auth status` and the docs can reference an unambiguous string.
func OCIGuestManifest ¶
func OCIRootFS ¶
OCIRootFS builds the machine.OCIImage rootfs spec for sb. Every caller (provider Create pre-build, vzd boot) must produce the identical value or the digest-keyed disk cache misses and a second disk gets built.
func RecordedGuestABI ¶
RecordedGuestABI is sb's guest ABI, resolving the zero value (records written before the field existed) to ABI 1.
func RepoShareTag ¶
RepoShareTag is the virtio-fs tag for a managed repo's src_ alias (the device that re-exposes the repo at its original host path so worktree .git backpointers resolve in the guest). Keyed on the full repo path.
func SaveOAuthToken ¶
SaveOAuthToken persists the token at ~/.clawk/claude-oauth-token with mode 0600. Atomic via write-tmp-rename so a Ctrl-C mid-write can't leave an empty file.
func SeedClaudeMemory ¶
SeedClaudeMemory writes seed into the agent's auto-memory entrypoint (<stateRoot>/claude/memory/MEMORY.md — the path the autoMemoryDirectory setting points at) the FIRST time a sandbox boots, when no memory file exists yet. It never overwrites: once the agent — or a prior session folded in via internal/sessions — has written memory, that wins. An empty seed or an already-present file is a no-op. Best-effort like SeedClaudeStateDir: failing to seed baseline knowledge must not block boot.
func SeedClaudeStateDir ¶
SeedClaudeStateDir writes the host-snapshot files (settings.json, CLAUDE.md, .credentials.json) directly into the per-sandbox state dir that PersistentClaudeShares mounts at ~/.claude/. Run by the provider during sandbox preparation, BEFORE the VM boots — so by the time virtio-fs mounts the dir, the files are already in place.
Semantics:
settings.json: overwritten on every call. Lets clawk refresh its forced overrides (bypass permissions, etc.) and propagate host settings.json edits into already-persisted sandboxes on re-create.
CLAUDE.md: overwritten on every call from host ~/.claude/CLAUDE.md (if present). Matches the old snapshot-at-create semantics exactly — host edits flow into newly-created sandboxes.
.credentials.json: written ONLY when (a) no long-lived OAuth token is configured (env var or clawk root file) and (b) the state dir doesn't already have a credentials file. The second condition preserves a refreshed token across destroy/recreate cycles, replacing the previous copy-in/copy-out dance through auth/credentials.json.
Returns the first error encountered. Best-effort: a missing host CLAUDE.md isn't an error (there's no source to copy), but failure to write into the state dir is.
Types ¶
type Bar ¶
type Bar struct {
// Label names the item, shown left of the bar (e.g. "layer 3/12").
Label string
// Frac is the completion fraction, 0..1.
Frac float64
}
Bar is one labelled sub-progress bar (e.g. a single layer's download) rendered on its own line beneath the current step.
type CaptureExecProvider ¶
type CaptureExecProvider interface {
ExecCapture(sb *config.Sandbox, command ...string) (output string, err error)
}
CaptureExecProvider is optionally implemented by providers that can run a guest command non-interactively and return its combined output. Narrated hooks (`on create` / `on up`) use it to keep successful command output off the screen and show it only on failure.
type ExitError ¶
type ExitError struct{ Code int }
ExitError reports that an interactive guest session (a shell or an attached agent) finished with a non-zero exit status that should become clawk's own process exit code.
Provider Shell methods and the CLI's interactive-session helpers return it instead of calling os.Exit in place: that lets the cobra command's deferred cleanup run and keeps a single os.Exit site in cmd/clawk, which unwraps it with errors.As. Carrying the status as an error is also what preserves the exact code — a plain returned error collapses to exit 1.
It is deliberately NOT used for internal command execution (ExecCapture, phase-setup scripts): those failures are ordinary errors that should surface a message and exit 1, not hijack the process exit code.
type FirecrackerProvider ¶
type FirecrackerProvider struct {
// contains filtered or unexported fields
}
FirecrackerProvider implements the Provider + agent interfaces using the machine/firecracker backend.
func NewFirecrackerProvider ¶
func NewFirecrackerProvider(store *config.Store) *FirecrackerProvider
func (*FirecrackerProvider) Create ¶
func (f *FirecrackerProvider) Create(sb *config.Sandbox) error
Create stages everything the VM boots from: the firecracker-CI kernel, the cross-compiled guest binaries, the OCI rootfs (with the worktree baked in), and the clawk-init manifest config disk.
func (*FirecrackerProvider) DaemonSpec ¶
func (f *FirecrackerProvider) DaemonSpec(sb *config.Sandbox, allow *netfilter.AllowList) (machine.Spec, error)
DaemonSpec sets up the host network plumbing (IP-less bridge + the guest's TAP + the daemon-owned gvproxy TAP) and returns the machine.Spec the __fcd daemon boots. It runs in the daemon process; the returned spec carries a UserMode net in TAP-bridge mode so the firecracker backend brings up gvproxy (with allow as the egress filter) bridged to the guest's NIC.
func (*FirecrackerProvider) Destroy ¶
func (f *FirecrackerProvider) Destroy(sb *config.Sandbox) error
func (*FirecrackerProvider) Exec ¶
func (f *FirecrackerProvider) Exec(sb *config.Sandbox, command ...string) error
Exec runs a command in the guest over the vsock agent. Used for the coding-agent attach (claude), so it's interactive.
func (*FirecrackerProvider) ExecCapture ¶
ExecCapture runs a command non-interactively and returns its combined output over the agent's frame protocol.
func (*FirecrackerProvider) FCStateDir ¶
func (f *FirecrackerProvider) FCStateDir(sb *config.Sandbox) string
FCStateDir exposes the machine-library state dir to the __fcd daemon.
func (*FirecrackerProvider) GuestWorkspaceRoot ¶
func (f *FirecrackerProvider) GuestWorkspaceRoot() string
GuestWorkspaceRoot: firecracker has no `agent` user and no virtio-fs, so the worktree is baked into the rootfs under /workspace.
func (*FirecrackerProvider) Shell ¶
func (f *FirecrackerProvider) Shell(sb *config.Sandbox, workdir string) error
Shell opens an interactive login shell in the guest over the vsock agent.
func (*FirecrackerProvider) Start ¶
func (f *FirecrackerProvider) Start(sb *config.Sandbox) error
Start spawns the detached __fcd daemon — which owns gvproxy, the frame pump, and the firecracker VM for the VM's lifetime — and returns once the pty-agent answers over vsock. The VM must outlive this CLI invocation, so (like the vz provider) the work runs in a child process, not in-process.
func (*FirecrackerProvider) Status ¶
func (f *FirecrackerProvider) Status(sb *config.Sandbox) (string, error)
func (*FirecrackerProvider) Stop ¶
func (f *FirecrackerProvider) Stop(sb *config.Sandbox) error
Stop signals the __fcd daemon, which tears down the VM, the frame pump, and gvproxy. A missing/stale pidfile is not an error.
The timeout MUST exceed the daemon's own graceful-stop budget (gracefulStop gives m.Stop 15s: CtrlAltDel wait 10s + SIGTERM-firecracker wait 5s). The minimal OCI guest doesn't power off on CtrlAltDel, so the daemon always spends ~10s there before falling through to SIGTERM the firecracker child. If we SIGKILL the daemon before that completes, firecracker is orphaned — it keeps the guest TAP open, and the next boot fails with "Open tap device failed: Resource busy". 25s leaves margin over the 15s budget plus the daemon's post-stop cleanup.
type HostFile ¶
type HostFile struct {
HostPath string
Content []byte
GuestPath string
Mode uint32
Owner string // "user:group" or "" for root
}
HostFile is a single host file snapshotted into the VM at boot from the guest manifest. Static — edits on the host require a fresh sandbox (or a future `clawk sync`) to propagate. Used for items that don't fit virtiofs's dir-only mount model.
Exactly one of HostPath or Content must be set. Content is used when the source isn't a file — e.g. a secret extracted from the macOS Keychain.
func ClaudeJSONMarkerFile ¶
ClaudeJSONMarkerFile returns the HostFile that pre-creates ~/.claude.json. Two concerns share the file:
- hasCompletedOnboarding (only when hasToken — the keychain credentials path doesn't need it).
- hasTrustDialogAccepted=true for every phase worktree path the sandbox mounts, the per-repo source mounts those worktrees point at, and the workspace root. Without these, a fresh sandbox prompts "is this a project you trust?" on first claude launch in each directory — a 100% pointless gate in a VM whose only contents the user just chose to mount.
Always emitted: the trust block is sandbox-shape-dependent (we only know the paths after PrepareVM resolves phases), but it's always wanted.
func DefaultHostFiles ¶
DefaultHostFiles returns host files snapshot-copied into each sandbox via the guest manifest. Today, the only items here are those that live OUTSIDE the per-sandbox ~/.claude/ mount:
- /etc/profile.d/98-clawk-claude-oauth.sh (env var for the long- lived OAuth token, when configured)
- ~/.claude.json (onboarding marker, at home root, not inside ~/.claude/)
- ~/.gitconfig
- ~/.ssh/known_hosts
Files that DO live inside ~/.claude/ — settings.json, CLAUDE.md, .credentials.json — are pre-seeded into the per-sandbox state dir by SeedClaudeStateDir before the VM boots; they appear at the canonical paths through the PersistentClaudeShares mount, no snapshot-file involvement needed.
clawkRootDir resolves the optional long-lived OAuth token (~/.clawk/claude-oauth-token, see oauth_token.go). When configured, the profile.d export and onboarding marker are emitted; the rotating keychain blob lives only in the seeded state dir.
func EnvFile ¶
EnvFile synthesizes an /etc/profile.d script that exports every sandbox-required env var. Names come from sb.RequiredEnv (declared in clawk.mod); values come from the host's process env at this call. Missing host vars log a warning and are exported empty — callers decide whether that's fatal (e.g., an MCP server failing to auth is a clear signal).
Written into /etc/profile.d/99-clawk-env.sh so every login shell (ssh, `claude ...`, interactive bash, phase setup scripts) picks up the values without any per-tool configuration.
Returns ok=false if the sandbox has no required env — saves the caller from having to filter empty HostFiles.
func OAuthTokenEnvFile ¶
OAuthTokenEnvFile returns the HostFile that drops an /etc/profile.d/ script exporting CLAUDE_CODE_OAUTH_TOKEN into every login shell inside the sandbox.
Mode is 0644 root-owned, deliberately readable by the agent user — /etc/profile.d/*.sh that aren't readable get silently skipped by /etc/profile, and the only non-root principal that ever runs inside the VM is `agent` anyway. The token is no more sensitive than the ~/.claude/.credentials.json blob already shipped.
The value is shell-escaped (backslash-escaping backslash, dollar, backtick, double-quote) so tokens that happen to contain shell metacharacters survive the export. Claude Code tokens are base64-ish today, but pinning that assumption into the export layer would silently break the day Anthropic changes the format.
func WorkspaceDocFile ¶
WorkspaceDocFile returns a CLAUDE.md that gets dropped at /home/agent/workspace/CLAUDE.md on first boot. Claude Code auto-loads a CLAUDE.md in its startup CWD, so whatever's in here becomes part of the agent's instructions the moment it starts.
The content describes WHERE the agent is (a clawk VM, not the user's laptop), WHAT the layout looks like (per-phase worktrees), and WHICH constraints apply (egress allowlist, mounts, git/ssh setup). Knowing these upfront stops the agent from wasting turns investigating the environment or being surprised by a blocked connection mid-task.
type HostShare ¶
type HostShare struct {
}
HostShare is a host directory shared into every sandbox. Tag must be unique across all shares on one VM (vz uses it as the virtio-fs mount identifier). GuestPath is where the guest mounts the share inside.
NinePVSockPort, when non-zero, marks the share for the 9p-over-vsock transport: the host runs a ninep server (internal/ninep) rooted at HostPath on that guest vsock port, and a 9p-capable clawk-init mounts it over 9p instead of virtio-fs. The virtio-fs device (Tag) is still attached as the fallback for older guests. Used for the toolchain caches, whose file counts make Apple's virtio-fs exhaust the host's kern.maxfiles. Zero = virtio-fs only (every other share). See ToolchainCacheShares.
func DefaultHostShares ¶
func DefaultHostShares() []HostShare
DefaultHostShares returns host agent capability dirs that are safe to live read-write-share across host and VM.
We do NOT share all of ~/.claude, even though that would match local multi-terminal behavior. The Claude Code issue tracker documents real corruption from concurrent access:
- anthropics/claude-code#28847 (.claude.json race corrupts state)
- anthropics/claude-code#25609 (OAuth refresh race revokes tokens)
- anthropics/claude-code#10039 (macOS Keychain deletes .credentials.json, breaking Linux co-mounted sessions)
Sharing Claude agents/commands and Codex skills is safe: user-authored capability dirs with low write contention. Sharing .claude.json, credentials, projects/, file-history/, or the whole ~/.codex state dir would risk concurrent writers. Each sandbox authenticates independently — one-time cost per sandbox in exchange for no cross-session corruption.
~/.claude/skills is deliberately NOT shared. A skill like gstack carries a large node_modules tree, and virtio-fs caches an inode (and host fd) per file it touches; across several running sandboxes that inflates the Virtualization.framework XPC processes' fd count enough to exhaust the host's system-wide open-file table (kern.maxfiles → ENFILE). Skills that a sandbox needs can be brought in per-sandbox via an explicit `shares (...)` entry instead.
func PersistentClaudeShares ¶
PersistentClaudeShares returns the per-sandbox host share that carries Claude Code's entire ~/.claude/ across destroy/recreate cycles. Call sites already resolved a Store so they pass the state root directly rather than re-deriving the path.
The host directory is created idempotently so virtiofs never encounters a missing source. The guest-side mount point is ~/.claude/ — per-sandbox storage that does NOT suffer the shared- .claude.json races documented below; two sandboxes can't touch the same path because each sandbox name maps to a distinct host dir.
We mount the whole dir rather than a curated subdir list because:
- The "ephemeral" subdirs (cache/, paste-cache/, shell-snapshots/, telemetry/) measure in hundreds of KB total — exclusion isn't worth the bookkeeping.
- settings.json and CLAUDE.md, formerly snapshot HostFiles, are now seeded via SeedClaudeStateDir straight into the synced dir before the mount happens. That removes the snapshot-file vs share-mount layering issue.
- .credentials.json (non-token path) lives at its canonical place inside the synced dir, so claude's atomic write-rename refresh persists naturally — no more copy-in/copy-out trick via auth/credentials.json.
Mount ordering: this share must come BEFORE DefaultHostShares in the assembled share list. The agents/commands sub-mounts land on top of ~/.claude/ at boot, and Linux would shadow them under a later parent mount.
Cross-sandbox races (the ones DefaultHostShares is avoiding) don't apply here — each sandbox name maps to its own state dir, so two sandboxes never write to the same path.
Opt out by passing an empty stateRoot.
func ToolchainCacheShares ¶
ToolchainCacheShares returns host shares that back the dependency caches of common language toolchains. Mounting one host directory per cache means a module/crate is downloaded once and reused across every sandbox — preventing the multi-GB-per-sandbox blowup we observed where each new clone re-downloaded the same Go module set.
Each guest path is the toolchain's *default* cache location, so no env vars and no provision.sh changes are required: the mount is transparent. Host dirs are created on demand (virtiofs refuses missing source paths); MkdirAll failures silently drop the offending share rather than failing sandbox creation, matching the behavior of PersistentClaudeShares.
Caches included (well-documented concurrent safety, real payoff):
Go modules ~/go/pkg/mod — file-locked, append-only;
standard CI sharing pattern.
Cargo registry+git ~/.cargo/registry/{index,cache}, ~/.cargo/git/db
— Cargo holds cross-process
locks; the same set every
Rust GHA workflow caches.
Caches deliberately excluded:
Go build cache (~/.cache/go-build) — NOT shared. It stays per-VM on the rootfs (Go's default location). Two reasons: (1) the build cache is not safe for concurrent builds against one directory (golang/go#43645) — racing `go build`s in different sandboxes can delete/move entries mid-read and corrupt each other's lookups, unlike the append-only, file-locked module cache; (2) it's the churniest tree we'd share — rewritten on every build — and over Apple's virtio-fs each guest-cached inode pins a host fd in the VM XPC process, so it dominated the descriptor blowup we saw. A build cache is cheap to repopulate per-VM, so the sharing payoff never justified the hazard. pnpm store / uv cache / Bun cache — all three hardlink from cache into the working tree (node_modules, .venv). Hardlinks don't cross filesystems, so on a virtiofs mount these tools silently fall back to copying — strictly worse than no sharing, since you pay the copy cost AND lose dedup. Revisit only if worktrees move onto the same shared mount layout. Cargo target/ — locked per-project; cross-project sharing is an unimplemented Rust project goal. ~/.cargo/bin and ~/.cargo/registry/src — bin/ holds executables, not cache; src/ is cheap to re-extract from cache/ and sharing it would just balloon disk usage. Zig and pip — small footprint here; pip is superseded by uv.
Pass cacheDir = "" to opt out.
func UserHostShares ¶
UserHostShares converts a sandbox's user-declared shares (config.HostShare from `shares (...)` in clawk.mod) into the internal HostShare form with stable virtio-fs tags.
Tags are derived from the guest path so the same share survives `clawk down && clawk up` without re-randomising — the guest manifest, the provider's --device list, and the runtime mount loop all key off the tag, and a churned tag means a stale mount entry that fails on next boot. SHA-256 hex prefix gives 8 hex chars (32 bits), collision-resistant inside a single sandbox's share list while staying short enough for virtio-fs tag length constraints.
type MockProvider ¶
type MockProvider struct {
Created []string
Started []string
Stopped []string
Destroyed []string
Running map[string]bool
}
MockProvider is a test double for the VM provider.
func NewMockProvider ¶
func NewMockProvider() *MockProvider
func (*MockProvider) Exec ¶
func (m *MockProvider) Exec(sb *config.Sandbox, command ...string) error
Exec implements ShellProvider for testing.
type OAuthTokenSource ¶
type OAuthTokenSource string
OAuthTokenSource describes where a token came from. Used by `clawk auth status` so the user can see which copy is winning — the env var trumps the file on every Claude Code invocation, and a surprised user otherwise can't tell why edits to the file aren't taking effect.
const ( OAuthTokenSourceNone OAuthTokenSource = "" OAuthTokenSourceEnv OAuthTokenSource = "env" OAuthTokenSourceFile OAuthTokenSource = "file" )
func LoadOAuthToken ¶
func LoadOAuthToken(clawkRootDir string) (string, OAuthTokenSource)
LoadOAuthToken returns the long-lived Claude Code OAuth token to propagate into sandboxes, along with which source it came from. A token == "" / source == OAuthTokenSourceNone means none configured.
Resolution order matches the precedence we want Claude Code itself to see at runtime:
- CLAUDE_CODE_OAUTH_TOKEN in the host process env — convenient for CI and one-off shells.
- ~/.clawk/claude-oauth-token — what `clawk auth set-token` persists. Mode 0600.
Both forms produce the same effect inside the sandbox: an env var exported via /etc/profile.d/. Whitespace is trimmed so users who piped through `pbpaste` and got a trailing newline aren't surprised by a "token doesn't look valid" error inside the VM.
type PlainProgress ¶
type PlainProgress struct{}
PlainProgress is the no-frills fallback: one line per completed step, no spinner, no transient detail. Suitable for pipes and logs.
func (PlainProgress) Close ¶
func (PlainProgress) Close()
func (PlainProgress) Detail ¶
func (PlainProgress) Detail(string, ...any)
func (PlainProgress) SetBars ¶
func (PlainProgress) SetBars([]Bar)
func (PlainProgress) SetFraction ¶
func (PlainProgress) SetFraction(float64)
func (PlainProgress) Skip ¶
func (PlainProgress) Skip()
func (PlainProgress) Step ¶
func (PlainProgress) Step(format string, args ...any)
func (PlainProgress) StepDone ¶
func (PlainProgress) StepDone(format string, args ...any)
type Progress ¶
type Progress interface {
Step(format string, args ...any)
Detail(format string, args ...any)
StepDone(format string, args ...any)
// Skip abandons the current step silently — for steps that turn out
// to be cache hits, where a checkmark line would just be noise.
Skip()
// SetFraction attaches a completion fraction (0..1) to the current
// step — renderers draw it as a progress bar. Negative clears it
// (back to indeterminate). Implementations may ignore it.
SetFraction(frac float64)
// SetBars replaces the set of per-item sub-progress bars drawn under
// the current step — one bar per concurrent layer download. Nil or
// empty clears them. Implementations may ignore it.
SetBars(bars []Bar)
Close()
}
Progress narrates long-running provider work (image pulls, rootfs builds) to the user. Providers call it sequentially: Step begins a unit of work, Detail updates its live status line, StepDone replaces the step with a completion summary. Close stops any rendering; callers should defer it as soon as they obtain a tracker.
The CLI installs a spinner-based implementation on interactive terminals (see internal/cli); everything else gets PlainProgress.
type Provider ¶
type Provider interface {
// Create prepares the VM (image, config, etc.).
Create(sb *config.Sandbox) error
// Start launches the VM.
Start(sb *config.Sandbox) error
// Stop gracefully stops the VM.
Stop(sb *config.Sandbox) error
// Destroy removes all VM artifacts.
Destroy(sb *config.Sandbox) error
// Status returns the current VM state as a string.
Status(sb *config.Sandbox) (string, error)
}
Provider abstracts VM lifecycle management. Implementations: vz (macOS), firecracker (Linux).
type RootExecProvider ¶
RootExecProvider is optionally implemented by providers that can run a one-shot command as root inside the guest without relying on sudo. OCI-image sandboxes need this: arbitrary images frequently ship no sudo, but their agent runs as root and can spawn root children directly.
type ShellProvider ¶
type ShellProvider interface {
Shell(sb *config.Sandbox, workdir string) error
Exec(sb *config.Sandbox, command ...string) error
}
ShellProvider is optionally implemented by providers that can open interactive shells directly (vz over the vsock agent).
type WorkspaceProvider ¶
type WorkspaceProvider interface {
GuestWorkspaceRoot() string
}
WorkspaceProvider is optionally implemented by providers whose guest mounts phase worktrees somewhere other than sandbox.WorkspaceRoot. Firecracker boots a bare-root rootfs without an `agent` user, so its shares live under /workspace; vz creates the agent user via clawk-init and mounts under /home/agent/workspace.
Callers that need to address phase paths inside the guest (run.go's agentStartDir, up.go's runPhaseSetup) should query this interface when present and fall back to sandbox.WorkspaceRoot otherwise.