Changelog¶
Changelog¶
All notable changes to this project will be documented in this file.
Unreleased¶
Added¶
- An OOM-killed sandbox is no longer silent. A sandbox that hit its memory limit - or was picked by the host's OOM killer - used to just disappear, taking the agent with it and leaving nothing to explain why. On the bwrap backend devsandbox now watches the sandbox's cgroup for the whole session and reports every OOM kill three ways: a message on stderr as it happens, a
sandbox.oomaudit event, and a record in the sandbox's metadata thatdevsandbox sandboxes listshows in itsSTATUScolumn asoom-killed(the sandbox died) oroom-kills(N)(it survived, having lost N processes). The record outlives the killed process and is cleared when the next session on that sandbox starts. Nothing to configure. On bwrap it is active whenever[sandbox.resources]limits are set, which is what gives the sandbox a cgroup of its own and so the only state in which a kill is attributable to it rather than to anything else sharing the invoking shell's cgroup; a bwrap sandbox running without limits gets a weaker signal instead - if it exits on SIGKILL, devsandbox reports that and says plainly that it cannot tell whether the OOM killer was responsible. On docker and krun the container always has its own cgroup, so it is always active there, including for processes started bydocker exec. Three cases are not covered: a container launched withkeep_container = false(anonymous, so the engine cannot be asked for its cgroup), an OOM inside a krun guest (decided by the guest kernel, never reaching a host counter), and macOS (the engine's PIDs belong to the VM its daemon runs in). See OOM Tracking.
Changed¶
- The sandbox's
$TMPDIRis now cleaned up instead of growing forever. When a tool needs a directory the host can read -revdiffis the only built-in case, so this applies only when therevdiffbinary is on PATH - devsandbox points$TMPDIRat a directory shared with the host. Because$TMPDIRis global, every temporary file in the sandbox landed there: build caches, test scratch trees and agent scratchpads accumulated on disk indefinitely, where a normal/tmpwould have discarded them at exit. Launching now empties the directory when no other session for the project is live, and otherwise removes only entries untouched for 7 days, so a concurrent session's working files are never pulled out from under it. See Shared temp directory. - That directory moved from
~/.cache/devsandbox/revdiff-ipc/<session>/to~/.cache/devsandbox/tmp/<session>/, and is no longer owned by therevdifftool: any tool can now declare it needs a host-visible temp directory and get the mount, the$TMPDIRexport and the cleanup from one place. The old directory is removed automatically the first time a project launches. Nothing needs to be reconfigured. - Embedded
pastaupgraded to passt2026_07_28.f8df3f1(from2026_07_16.090d739).
Fixed¶
- Fixed a plain bwrap launch logging
session.startwith no matchingsession.end, leaving every such session looking unfinished in the audit trail. That launch replaced the devsandbox process with bwrap outright, so whatever would have written the closing event was gone before the session ended - and nothing host-side could observe an OOM kill either, which is why the launch now keeps a parent process. The exit is unchanged: devsandbox returns as soon as the sandbox does, propagates its exit status, and dies of the same signal when the sandbox was terminated, so an interrupted sandbox still reports as interrupted rather than as a devsandbox failure. See Resource Limits.
v0.18.0 - 2026-07-24¶
Breaking Changes¶
- bwrap proxy mode now requires
iproute2andnftoriptableson the host, and its sandboxes are IPv4-only. A--proxylaunch on a host missing them, or missing thenf_tables/ip_tablesandnf_conntrackkernel modules, now aborts naming what is missing instead of starting with egress open. There is no opt-out.devsandbox doctorreports the same check as theproxy: firewallrow. Non-proxy launches are unaffected. See Requirements. - bwrap proxy mode now also requires a
pastathat supports--map-host-loopback. That option maps the proxy gateway, which is the lockdown's only permitted destination, so on an olderpastaevery connection would hang until it timed out. The launch is now refused up front. The embeddedpastaalways supports it, so this is reachable only withuse_embedded = falseor a failed extraction. - The container image no longer bakes in
gh(GitHub CLI) orneovim; the image is slimmer. Rebuild with--build-arg INSTALL_GH=true/INSTALL_NVIM=true, or add them in a derived image. Their host configs are still mounted as before. The Dockerfile also gainedSANDBOX_HOME,SANDBOX_UID,SANDBOX_GIDandNODE_VERSIONbuild args.
Added¶
-
The standalone GitHub Copilot CLI (
npm @github/copilot) now works sandboxed. Its home~/.copilot- config, MCP servers, sessions and auth - is mounted read-write and persistent, so a sandboxedcopilotruns authenticated andcopilot --resume/--continuefinds sessions from an earlier run. The oldergh copilotextension keeps its existing~/.config/github-copilotand~/.cache/github-copilotmounts. See Tools: GitHub Copilot. -
New
rtktool, so the rtk CLI proxy keeps its configuration and history inside the sandbox. Your filters andconfig.tomlnow apply in the sandbox, and the tracking database behindrtk gainaccumulates across runs instead of being recreated empty on every launch. The host's database is never modified. See Tools: rtk CLI Proxy. - herdr terminal workspace support, through a capability-filtering proxy. Running devsandbox inside a herdr session lets sandboxed tools open a review overlay in a herdr tab -
revdiffis the first consumer. The host control socket is never bind-mounted; a proxy is, and it permits only the methods enabled tools declare, denying the rest of the 86 herdr exposes. Configure with[tools.herdr] mode = "auto" | "disabled" | "enforce". See herdr Terminal Workspace. - herdr can now capture and restore a sandboxed agent's native session. A new
agent_reportingcapability permits three reporting methods and nothing else, so herdr can record an agent's own session ID or transcript path and resume it after a restart. It activates only for a directdevsandbox claude|pi|codexlaunch inside a herdr pane, because that is the only case where devsandbox knows both anchors every report is checked against; starting a shell and typingclaudeinside it does not enable it. Behavior change: such a launch previously started no proxy and now starts the filtered one, and a herdr control socket that cannot be reached is no longer fatal in the defaultautomode. See Agent session capture and restore. - Launching an agent from a herdr pane now records which sandbox owns that pane, so a resume cannot silently start a new session.
run-agentrefuses a resume-shaped invocation unless re-entry would reach the same project directory and the same sandbox state root. A--worktreelaunch is the case that makes this necessary: its state root is derived from the repo root while the session runs in the worktree, so re-entering would open a different session store and begin a fresh session while appearing to resume. - New
devsandbox agent-wrappers activate <shell>, so supported agents run sandboxed by default. It prints shell functions for you to evaluate from your own startup file, the waymise activatedoes -devsandbox agent-wrappers activate fish | source, oreval "$(devsandbox agent-wrappers activate bash)". Typingclaudethen runsdevsandbox claudein the current directory with every argument passed through, whileclaude-no-dsandcommand claudestill reach the real binary.claude,pi,codex,opencode, andcopilot(the standalone GitHub Copilot CLI) are wrapped when installed. Nothing is written to disk and no startup file is ever edited. Because the definitions are regenerated at every shell start they cannot go stale: a newly installed agent, or an upgrade that moved the devsandbox binary, is picked up by the next shell. Nothing is wrapped inside a sandbox. The feature is independent of herdr and useful on its own. See Tools: Shell wrappers. - The wrappers never resolve devsandbox through
PATH. Each definition carries the absolute pathactivateresolved for itself, and a path that has disappeared fails closed withdevsandbox: no executable at <path>and exit 127 rather than falling through to the unwrapped agent or looking devsandbox up inPATH, which may name a directory sandboxed code can write to. Only the once-per-shell-startactivatecall itself goes throughPATH, so an upgrade that moves the binary self-heals. - New
devsandbox run-agent <agent> [args...]command, the entrypoint for the wrappers. It forwards every argument untouched, sorun-agent claude --resume IDreaches the agent instead of being parsed as a devsandbox flag, and it executes the real agent directly when already inside a sandbox so a wrapper visible in-sandbox cannot recurse. - Resource limits are now available on every isolation backend, under a backend-neutral
[sandbox.resources]section.memory,cpusand the newpidsare configured in one place instead of the docker-scoped block they used to live in. Limits are opt-in, and one that cannot be enforced aborts the launch rather than silently running unlimited. See Resource Limits. - The bwrap backend can now cap memory, CPU and process count. A runaway build or fork bomb previously consumed host resources unchecked on the default Linux backend. The sandbox now runs inside a systemd transient scope carrying the equivalent cgroup v2 controls, which requires cgroup v2 and a systemd user session with the needed controllers delegated.
memorybounds resident memory but not swap, which is weaker than docker and krun. See Sandboxing: Resource Limits. - The docker backend now honors
pidsas--pids-limit, so a fork bomb inside the sandbox cannot exhaust the host's PID space. The first run after upgrading recreates any kept container once. krun skips the flag and warns: a host-side cap would limit the VMM's own threads, not the guest's processes. - The container backends (
dockerandkrun) now make host-installed mise tools available inside the sandbox on Linux hosts. The host's~/.local/share/mise/installsis mounted read-only and mirrored into the sandbox mise data dir on startup, so your host toolchain resolves in-guest without reinstalling or network access - previously the main reason a mise-centric workflow felt unusable underkrun. Versions installed inside the sandbox still take precedence. Host tools compiled against a newer glibc than the guest image may not run there, and macOS hosts share nothing. See Tools: mise. - New
[tools.mise] ignore_global_configoption to stop the sandbox reading the host's global mise config. A large host config full of@latestspecs made every shell start resolve them over the network, which on a proxy or egress-locked sandbox hangs and can OOM the guest. Setting it pointsMISE_GLOBAL_CONFIG_FILEat/dev/nullin the sandbox; the project.mise.toml, the image's system config andsettings.tomlstill apply. Defaults tofalse. - Experimental
krunmicroVM isolation backend.--isolation krunruns the same sandbox image inside a libkrun microVM, so the workload gets its own guest kernel behind a hardware virtualization boundary - the right boundary for genuinely untrusted code, where a host-kernel exploit must not reach the host. It is opt-in, never auto-selected, ephemeral, and runs rootless. Requirespodman, acrunbuilt with libkrun, and/dev/kvmon Linux or Apple Silicon on macOS;devsandboxfails fast with installation guidance when a prerequisite is missing. See krun microVM backend and the getting-started guide. - Egress lockdown: in proxy mode krun installs a host-side, deny-by-default firewall in the VMM's network namespace, permitting only loopback, established traffic and TCP to the proxy port - so the LAN, cloud metadata at
169.254.169.254, external DNS and every non-proxy gateway port are closed without being enumerated. It runs host-side because under libkrun TSI the guest has no routable interface. The in-guest shim waits for it before running anything, so untrusted code never runs with egress open, and any failure tears the microVM down. See krun backend. - Guest tooling: an egress-locked guest runs mise offline, so
@latestspecs resolve instantly from seeded host installs instead of stalling on lookups that never traverse the proxy. krun runs no boot-time install pass, so a tool your.mise.tomlpins that is neither seeded nor already present must be installed in-sandbox withMISE_OFFLINE=0 mise install. - Overlay copies: krun copies overlay and
tmpoverlaytool directories into the guest, since it cannot mount kernel overlayfs over virtio-fs. Sockets, FIFOs and device nodes in the source are skipped as the runtime artifacts they are, instead of aborting the launch. - Management parity:
devsandbox doctorreports krun prerequisites as advisory rows with remediation,sandboxes listandsandboxes prunecover ephemeral krun sandboxes, andforwardis best-effort. A second launch while a session is already active fails fast with a copy-pasteablepodman rm -f, instead of aborting only after the image was rebuilt.
Changed¶
- bwrap proxy mode is now enforced, not best-effort. The old route surgery discarded its own errors and exec'd the workload regardless, so any host where
ipis not on the user'sPATHstarted with egress fully open and nothing reported it. Even where it applied, the sandbox's own subnet stayed reachable (deleting the default route does not remove the on-link route), so the router UI, a NAS, the LAN DNS resolver and a cloud host's169.254.169.254metadata endpoint were all still directly reachable, along with every host loopback port and all of IPv6. A proxy-mode sandbox now gets the same deny-by-default lockdown krun applies, installed before the workload exists, and any failing step aborts the launch. This scopes the sandbox's path to the proxy, not its reachable destinations - those are still decided by HTTP filtering, so a metadata or LAN address is refused as a direct socket and still served through the proxy, where it is visible and refusable. Configured outbound port-forwarding rules keep working; host loopback ports that were not configured as one are no longer reachable at the gateway. See Backend-specific behavior. devsandbox doctorchecks the proxy firewall prerequisite for every backend, and checks it by using it. The oldkrun: firewallrow only looked for annftoriptablesbinary, and its name told bwrap users - the default backend, which now also aborts a--proxylaunch without a working firewall - that it did not concern them. The new top-levelproxy: firewallrow applies the real lockdown rule set in a throwaway namespace, so a host with nftables installed but no loadablenf_conntrackis reported here instead of failing mid-launch. It stays advisory.- Resource limits moved to a backend-neutral
[sandbox.resources]section, and gained apidslimit.[sandbox.docker.resources]still works but is deprecated, and it stays scoped to the container backends, which merge it field by field with the new section. bwrap does not read the deprecated block at all: it never honored it, and since it aborts when a limit cannot be enforced, applying a docker-oriented config there would turn a working setup into a failed startup on any host without a systemd user session. Validation errors now name the block they came from. See Resource Limits. devsandbox doctorno longer closes with "All checks passed!" when rows warned. A run with no failures but one or more warnings now ends withAll required checks passed (N advisory warning(s)), and the "How to fix" block sits directly above that line instead of being separated from it by the tools table. The exit code is unchanged - only errors fail the run.- Embedded
pastaupgraded to passt2026_07_16.090d739(from2026_06_11.a9c61ff). Embeddedbwrapis unchanged atv0.11.2. - krun warns when building a project-provided Dockerfile, whose
RUNsteps execute on the host - outside the microVM boundary and outside the proxy egress lockdown. The build still proceeds; the auto-generated default and every docker-backend build stay silent. See Build-time trust boundary. - Docker and krun startup timeouts now report the container's logs instead of a bare deadline. The in-guest shim's fatal output went to the container log and never reached the user, so a launch that failed a second into setup surfaced only as
container setup timed out after 90s. The last 50 log lines are now appended, best-effort and never masking the original timeout. - Proxy-mode, content-redaction and
.envmasking claims now state their actual limits wherever they appear.--help, the config template, the README, the landing page and the guides all described proxy mode as routing or logging all HTTP(S) traffic and.envmasking as unconditional. Neither holds: Docker is env-var routing with no network-level enforcement, and.envmasking scans 3 directory levels below the project root, skippingnode_modules,.git,vendorand.venv. Content redaction only sees requests that reach the proxy, and HTTPS bodies, headers and URLs only when MITM is enabled. See Redaction Coverage. Documentation only.
Fixed¶
- Proxy mode no longer leaves the host's own loopback services directly reachable from the sandbox. pasta forwards host ports into the namespace unless told otherwise, and loopback is the one interface the egress firewall must permit - so a local database or dev API stayed reachable at
127.0.0.1:<port>even though the same service was correctly refused at the proxy gateway. Proxy-mode launches on bwrap and krun now disable automatic forwarding for every protocol without a configured outbound port forwarding rule. - Claude and Pi sessions started inside the sandbox are no longer discarded on exit. Their persistent session overlays were skipped when the host directory did not exist - exactly the case for a user who authenticated on the host and only ever runs the agent sandboxed - so every transcript vanished with the sandbox and herdr would resume into nothing. devsandbox now creates those directories on the host when the agent's own directory exists.
CLAUDE_CONFIG_DIRandPI_CODING_AGENT_DIRare honored. claude -c/claude --continueare now covered by the herdr worktree guard. The guard recognized only herdr's own resume argv, but--continuereopens the same per-project session store--resumedoes - so in a pane launched with--worktreeit silently began a new conversation.- A herdr pane record that cannot be parsed now names its file. The failure refuses every resume in that pane, and deleting the file is the only way out - which the old error gave no way to find.
- Codex sessions started inside the sandbox are no longer discarded on exit, so
codex resumecan find them. All of~/.codexwas mounted as a config directory and got a tmpoverlay, so every rollout file vanished with the sandbox and a later resume reportedno rollout found for thread id.~/.codex/sessionsis now bound separately with a persistent overlay while the Codex home keeps its tmpoverlay, and devsandbox creates the directory on the host when it is missing.CODEX_HOMEis honored. revdiffcan now actually open its review overlay in a herdr tab. The launcher shell-quotes the generated script path, sendingsh '<path>', but the proxy recognized only the unquoted form - so every launch was denied and the launcher reported onlyherdr pane run failed. The proxy now strips one layer of single quotes before validating the path; a remainder still containing a quote is left alone and still rejected.- The docker backend no longer aborts startup with
fork overlay child: operation not permitted. Realizing atmpoverlaydirectory required namespace flags that Docker's default seccomp profile denies withoutCAP_SYS_ADMIN, so effectively everyisolation = "docker"sandbox failed to start. Those directories now use the same copy-on-start path macOS and krun already used, so no capability or seccomp relaxation is needed and a previous run's writes are still never visible. - The
krun: system pastadoctor row says when its warning may be a false positive. The probe only searches$PATH, while podman also looks inhelper_binaries_dir, so a host that installs pasta there was told to install what it already has. The remediation now names the limitation and gives the command that settles it. - The kitty proxy no longer accepts a launch command that names a sandbox-planted binary. Launch patterns matched
argv[0]on basename alone, and the revdiff IPC directory is a write-through bind mounted at an identical path on the host - so sandboxed code could drop its own executable there, name it in akitty @ launchrequest, and have kitty run it on the host as the host user. Patterns are now pinned to the program's resolved absolute path, and an unresolvable binary denies every launch rather than falling back. - A second session for the same project no longer breaks a running session's notifications, Docker access and kitty remote control. The portal, Docker and kitty sockets were keyed only on the project, so a second session unlinked the live session's socket and its exit deleted the path outright. Each session's sockets now live in a directory private to the owning process;
DOCKER_HOSTandKITTY_LISTEN_ONpoint at$HOME/.run/<pid>/. - A socket path too long for the kernel is now reported as such.
bind(2)rejects anything past 107 bytes with a bareinvalid argument, which the portal surfaced only as an opaque timeout. The portal, Docker, kitty and herdr proxies now report the path, its length, the limit and the remedy. - Proxy-mode sandboxes no longer stall for minutes resolving
@latestmise tool specs. Some backends' lookups never traverse the proxy and hang to their 20s timeout, and mise re-resolves per listed row - a singlemise lswas measured at 14 minutes. All proxy-mode sandboxes now bound remote lookups at 3s, and a value you set through the sandbox env config takes precedence. Under krun the offline-mise layer removes the lookups entirely: the samemise lswent to under a second. tmpoverlayconfig dirs that degrade to a copy-on-start overlay (krun anywhere, docker on macOS) are now reset to the host source on every run. The copy only wrote source entries and never removed extraneous ones, so anything a previous - possibly untrusted - run left under the target survived into the next session, defeating tmpoverlay's discard-on-exit promise. The clear is mount-aware, preserving nested read-only bindings, and it removes rather than follows any symlink planted at the target or an intermediate path component.- Docker isolation no longer hangs at startup for non-root users when a tool uses an overlay mount.
CAP_DAC_OVERRIDEwas granted only to krun, so under rootful Docker container-root could not read the host-owned overlay manifest and exited before signalling ready - the launch then waited out the 90s readiness timeout. It is now granted on docker too, scoped to the shim's root setup phase; the workload never holds it. - A kept Docker container is now actually reused when a tool uses an overlay mount, instead of being destroyed and rebuilt on every launch. The overlay manifest was a per-run temp file that the container binds permanently, so every later start mounted a path that no longer existed and fell back to recreating the container -
keep_containerbought nothing and all container state was lost each run. The manifest now lives at a stable per-project path and is rewritten in place. - A container whose startup fails now reports the failure immediately, instead of after the 90s readiness timeout. The readiness probe only polled for the shim's ready sentinel, which fails identically for a container that is still booting and one that has already died. It now also checks container state and reports the exit code alongside the log tail.
- Piped stdin now reaches non-interactive krun and docker commands. The container ran without
-i, sodata | devsandbox --isolation krun - toolclosed the workload's stdin and silently lost the input. bwrap, which runs the workload as a direct child, was unaffected. - A sandboxed command's exit code now propagates to the host instead of collapsing to
1.devsandbox - sh -c 'exit 42'exited1on every backend, because a non-zero command result was treated as a generic CLI error - which also printed a spuriousError:line. Genuine setup failures still exit1loudly, and a container-engine launch failure (exit125) is surfaced as an error rather than passed off as the workload's own status. - The session and proxy lock files are no longer unlinked on release, and are opened with
O_NOFOLLOW. Unlinking reopened a split-lock race in which two holders could run at once, and the predictable temp path let a co-tenant pre-plant a symlink for the holder to truncate. - The in-guest shim no longer silently discards sandbox user/group creation errors, and
USERnames the real account. A faileduseradd/groupaddwas ignored, andUSERwas hardcoded tosandboxusereven when no such passwd entry existed.USERis now resolved from the passwd entry for the uid the shim drops to. - The container backends no longer reinstall the pre-baked node on every guest, and krun now persists mise-installed tools across runs like docker already did. The image's
node@22lives in the ephemeral image path, so without seeding it into the persistent data dir every fresh guest stalled for minutes reinstalling it, withnode is not a valid shimerrors alongside a large global mise config. The seed is version-level, so a node version the project installs itself still persists to the sandbox home. - krun now refuses to launch on an Intel Mac instead of failing obscurely later. There is no supported libkrun path on
darwin/amd64, but the prerequisite check had no architecture probe, so the launch only broke after the image build with an opaque runtime error. The check now runs first and points at--isolation=docker;devsandbox doctorreports it as akrun: platformrow. devsandbox config initno longer emits obsolete config keys. The template documented an[overlay] enabledswitch that no longer exists and emitted[tools.mise] writable/persistent, which mise no longer reads - so copying the template's own suggestions produced settings that silently did nothing. A round-trip test now asserts every key the generator emits is one the loader recognizes.- A
hiddenmount rule that hides nothing is now reported at launch, and its remediation names something that actually works. A pattern that resolves to a directory cannot be replaced by/dev/null, and the skip was only written to the log file - sopattern = "secrets/**"gave a fully readable secrets directory with nothing on the terminal to say so. The skip is now a startup warning, and every surface gives the only remedy there is: match the files inside the directory (**/secrets/**). devsandbox doctorno longer fails the run because past sandbox runs logged errors. Thelogsrow escalated to an error above 10 errors in 24h, sodoctorexited1with "Please install missing dependencies" on a host where nothing was missing. Recent log errors say nothing about whether a sandbox can launch now, so the row is advisory at any count - which makesdoctorusable as a CI gate again. The failure summary now names the rows that failed instead of always blaming missing dependencies.- The
hiddenmount mode is now documented as files-only, and the README no longer claims the sandbox exposes "nothing else". The config template and the schema comment both describedhiddenas hiding a file or a directory, while the builder only overlays files. The README's isolation summary also omitted the read-only host system paths and sanitized configs the defaults mount; what is not mounted - SSH keys, cloud credentials, sibling projects - is unchanged. Documentation only.
v0.17.3 - 2026-06-24¶
Added¶
DEVSANDBOX_DEBUG=1proxy lifecycle tracing. The MITM proxy now logs a per-requestCONNECT/request/responsetrace to the internal proxy log (devsandbox logs internal --type proxy), including response status, content-type, streaming detection, and time-to-headers. Query strings are stripped so tokens are never logged. Use it to pinpoint where a hung or timed-out request stalls. See Debugging the Request/Response Lifecycle.
Fixed¶
- MITM proxy no longer buffers response bodies before relaying headers. goproxy relays a response to the client only after the
OnResponsehandler returns and does not flush the body until the handler-suppliedresp.Bodyis read, but request logging read the entire body withio.ReadAllto capture it. For any streaming response the body stays open until generation finishes, so the proxy withheld the response headers for the full duration - codex aborted withCodex SSE response headers timed out after 20000mswhile the proxy spent 10-80s reading the stream (one HTTP upgrade buffered for 82s). Crucially these responses are not always identifiable byContent-Type(codex's streamed responses carry an emptyContent-Type), so media-type sniffing alone could not avoid the buffering. The response body is now wrapped so it streams to the client unchanged while a bounded prefix (256 KiB) is captured for logging; the log entry is written when the body closes. The proxy never buffers a body before relaying headers, so SSE, chunked, empty-Content-Type, and large responses all stream incrementally. This also unbreaks WebSocket (WSS) and other HTTP upgrades through MITM: 1xx/101 Switching Protocolsresponses are now left untouched, so goproxy can type-assertresp.Bodytoio.ReadWriterand relay the upgraded connection (the old code read the 101 body and replaced it with abytes.Reader, stalling the stream for up to 82s and failing the relay).
v0.17.2 - 2026-06-15¶
Changed¶
- Embedded
pastaupgraded to passt2026_06_11.a9c61ff(from2026_05_07.1afd4ed). The statically linked pasta binary that backs sandbox networking is rebuilt from the newer upstream passt release. Embeddedbwrapis unchanged atv0.11.2.
v0.17.1 - 2026-05-13¶
Fixed¶
- Proxy no longer panics on requests with a nil
URL. goproxy can dispatch HTTPS requests whosehttp.Request.URLis nil when its internalurl.Parsefallback fails (the parse error is swallowed and the request is still handed off). Every downstream step - credential injection, filtering, redaction, ask-mode, request logging - dereferencesreq.URL, so any such request crashed the proxy worker.RequestLogger.LogRequestnow falls back toRequestURIwhenURLis nil, and the request handler short-circuits with a 403 (malformed request: missing URL) instead of dispatching downstream.
v0.17.0 - 2026-04-30¶
Added¶
- Audit-grade structured logging. Per-session fields (
session_id,sandbox_name,sandbox_path,project_dir,isolator,pid,devsandbox_version) on every dispatched entry, plus synthesizedsession.start/session.endlifecycle events and security events (proxy.filter.decision,proxy.redaction.applied,proxy.credential.injected,proxy.mitm.bypass,mount.decision,notice.overflow). See Audit Logging. - OTLP
header_sources. Resolve receiver headers fromvalue/env/fileat runtime so secrets stay on the host. See Authenticating to an Auth-Enforced Endpoint. NODE_USE_ENV_PROXY=1is now set automatically in proxy mode so Node.js ≥24's built-infetch(undici) honorsHTTP(S)_PROXY- fixesENETUNREACHfrom npx-based tools likemcp-remote.
v0.16.0 - 2026-04-29¶
Added¶
- Proxy
log_skiprules. Drop matching requests from the proxy log (local + remote dispatchers); the request itself still passes through. See Skipping Log Entries.
v0.15.0 - 2026-04-29¶
Added¶
devsandbox sandboxes prune --orphanedflag to restrict pruning to orphaned sandboxes (those whose original project directory no longer exists). The flag intersects with other selectors:--orphaned --older-than 30dremoves orphans last used over 30 days ago;--orphaned --keep Nprunes orphans outside the N most-recently-used set;--orphaned --all(or--orphanedalone) removes every orphan. Without the flag, the existing default (orphans-only when no other selector is set) is unchanged.- Generic credential injector for proxy. Define credential injection by
host+header+value_format+[...source]+overwritein TOML - no Go code change required to add a new service. Built-ingithubpreset preserves existing config compatibility ([proxy.credentials.github] enabled = trueworks unchanged, includingGITHUB_TOKEN→GH_TOKENfallback). Specificity-based ordering when multiple injectors could match the same request (exact host > longer literal > shorter glob, tie-break by name).BuildCredentialInjectorsnow returns an error for invalid configs (unknown preset, missinghost/header, invalid glob).
v0.14.1 - 2026-04-28¶
Changed¶
zellijtool is now disabled by default. Unlikekitty, the zellij socket has no capability filtering - exposing it lets sandboxed code drive the host multiplexer (run commands in any pane, read pane contents, etc.). Auto-detection of an activeZELLIJsession no longer mounts the socket or forwardsZELLIJ*env vars on its own. Set[tools.zellij] enabled = trueto opt back in.devsandbox tools check zellijreports the opt-in requirement.
v0.14.0 - 2026-04-27¶
Added¶
codex,opencode, andpitools now honor their respective custom config-location env vars on the host and forward them into the sandbox so the CLIs resolve the same paths inside:codex:CODEX_HOMEoverrides~/.codex. When set, the host value is passed through and the directory is mounted at the same path.opencode:OPENCODE_CONFIG_DIRis mounted in addition to (not in place of)~/.config/opencode, matching opencode's load semantics; the env var is forwarded.pi:PI_CODING_AGENT_DIRoverrides~/.pi/agent. The agent dir is still tmpoverlayed (settings/credentials are write-discarded) and thesessions/subdirectory is still persisted; the env var is forwarded.
v0.13.3 - 2026-04-20¶
Fixed¶
kittyproxy revdiff launch pattern now accepts the unquoted/usr/bin/envprefix the launcher actually emits (onlyENV_PREFIXassignments and the inner argv are single-quoted). The literal absolute path is required - bareenv(PATH-relative) still rejects, so$PATHshadowing can't be used to bypass the inner-program check.
v0.13.2 - 2026-04-20¶
Fixed¶
kittyproxy revdiff launch pattern: addedMatchShellExecEnvSentinel, acceptingsh -c "'/usr/bin/env' 'KEY=VAL' ... '<prog>' '<arg>'...; touch '<sentinel>'". The revdiff launcher injects anenvwrapper so the kitty-spawned overlay inheritsEDITOR/VISUALfrom the caller's login shell; the previous pattern matched only the no-env form. Env-var names are restricted to^[A-Z_][A-Z0-9_]*$, the inner argv is still validated against the existing revdiff pattern, and the sentinel-tail rules (no shell metacharacters, canonical path) are unchanged.
v0.13.1 - 2026-04-18¶
Fixed¶
revdifftool no longer wipes its shared IPC directory onStart/Stop. Because the dir is exported as$TMPDIRfor every sandboxed process, long-lived tenants (Claude Code's per-session task cache under$TMPDIR/claude-<uid>/…/tasks/, Node's compile cache, Go's build cache) populate subtrees that must survive sandbox restarts for the same project - and parallel sandboxes on the same project share the directory, so wiping it from one tore state out from under the others. The oldRemoveAllonStartcould yank state out from under a running caller; Node's non-recursivefs.mkdirSyncthen failed withENOENT, breaking every subsequent Claude Code Bash tool call.Startnow only ensures the dir exists (0700);Stopis a no-op. Stale revdiff sentinels are harmless - the launcher usesmktempwith fresh names.
v0.13.0 - 2026-04-17¶
Added¶
[sandbox.environment.<NAME>]config block: declare sandbox environment variables using the same source model as proxy credentials (value/env/file, priorityvalue > env > file).env = "X"withXunset on the host silently skips the variable; an unreadablefile = "..."is a startup error. Declaring the same variable in bothenv_passthroughandenvironmentfails at startup with a message naming the variable - each variable belongs in exactly one place.pitool: integrates Pi Coding Agent.~/.pi/agentis mounted with credential protection;~/.pi/agent/sessionspersists across runs.[proxy.credentials.github] overwrite = true: force-replace any existingAuthorizationheader on outgoingapi.github.comrequests. Intended for the pattern where a sandboxed CLI (e.g.gh) refuses to start without a token in its environment - pass a placeholder throughenv_passthrough/sandbox.environmentwhile the real token stays on the host and is swapped in by the proxy. Default remainsfalse(existing tool-set headers are preserved).revdifftool now provides a shared IPC directory (~/.cache/devsandbox/revdiff-ipc/<session>/) bind-mounted at the same path on both sides and exported asTMPDIR. The kitty-spawned overlay shell runs on the host and receives sentinel/output paths as literal strings, so host and sandbox must agree on the string - argv-shipped paths needSource == Destequality, not just a shared inode.
Changed¶
- Kitty tool now runs a capability-filtering proxy instead of bind-mounting the host socket. The host kitty remote-control socket is no longer exposed inside the sandbox; a local proxy at
$HOME/.kitty.sockis exposed instead, andKITTY_LISTEN_ONis rewritten to point at it. Sandboxed processes can only issue kitty commands declared as capabilities by an enabled tool (launch_overlay,launch_window,launch_tab,launch_os_window,close_owned,wait_owned,focus_owned,send_text_owned,get_text_owned,set_title_owned,list_owned), and*_ownedcommands are scoped by ownership tracking to windows the sandbox itself opened. Shell metacharacters insh -cpayloads forlaunch_*are rejected outright.remote_control_passwordis unsupported - useallow_remote_control = socket-only. New[tools.kitty]fields:mode(autodefault /disabled/enforce) andextra_capabilities(additive;launch_*entries rejected). Underauto, the proxy only starts when at least one enabled tool declares a capability - zero attack surface when no tool needs kitty.revdiffis the built-in consumer.
Fixed¶
- macOS: shortened test directory names to stay under the platform's unix socket path length limit (affected
kittyproxyandkittytool tests).
v0.12.0 - 2026-04-16¶
Changed¶
- Wrapper diagnostic output (port-forward notices, session warnings, proxy setup info, container progress) no longer writes directly to stderr while a child process owns the terminal. Messages are written to
$XDG_STATE_HOME/devsandbox/wrapper.log(or~/.local/state/devsandbox/wrapper.log) and a one-line banner is shown on exit if anything was suppressed. This prevents wrapper output from corrupting TUI applications (Claude Code, aider, etc.) running inside the sandbox. Pass--verboseor setDEVSANDBOX_DEBUG=1to restore the old behavior of writing every message to stderr.
Added¶
--worktreeand--worktree-baseflags: opt-in git-worktree mode. Bare--worktreeauto-generatesdevsandbox/<session-or-timestamp>off HEAD;--worktree=<branch>reuses or creates a named branch. The sandbox CWD is the worktree; the main checkout is untouched. With--git-mode=readwrite, commits land on the worktree branch only.--rmremoves the worktree on exit viagit worktree remove --force+prune.--worktree+--git-mode=disabledis rejected at flag-parse time. Worktrees live at~/.local/share/devsandbox/<project-slug>/worktrees/<branch>/and the slug is derived from the main repo root so sibling worktrees share sandbox state.devsandbox sandboxes pruneand thedoctorcommand are worktree-aware.
Fixed¶
zellijandkittytool socket bindings are now explicit bind mounts (Type: MountBind) instead of inheriting the default tmpoverlay fromCategoryRuntime. Overlayfs cannot expose a unix socket from its lower layer, so under the previous policy the host socket was invisible inside the sandbox andzellij list-sessions/kitten @silently failed.- Auto port-forwarding no longer tries (and fails) to forward when the sandbox shares the host network namespace. Without proxy mode the sandbox uses bwrap's
--share-net, so a tool listener inside the sandbox is the same kernel socket as the "host" bind the forwarder would attempt - producing a spuriousbind: address already in useerror for every detected port. Auto-detect now inspects the sandbox netns inode and skips forwarding (with a one-line explanatory message) when it matches the host; the sandbox ports are already directly reachable on127.0.0.1. For the rare case where auto-forward runs in a properly isolated netns but the host happens to already have that port in use, the forwarder falls back to an ephemeral host port and logs the mapping instead of silently dropping the service.
v0.11.0 - 2026-04-14¶
Added¶
zellijtool forwards an active Zellij session into the sandbox by mounting the session socket directory and thezellijbinary. Auto-detected whenZELLIJis set and the binary is onPATH, sozellijcommands run inside the sandbox attach to the host multiplexer.zellijtool now also mounts$XDG_RUNTIME_DIR/zellij/, which is where zellij 0.41+ stores its IPC socket (the legacy/tmp/zellij-$UID/holds only cache/log files on modern releases). The override env var isZELLIJ_SOCKET_DIR(previously the tool checked the incorrectZELLIJ_SOCK_DIR).
v0.10.0 - 2026-04-10¶
Added¶
kittytool forwards the Kitty remote-control socket into the sandbox sokitten @commands inside the sandbox can drive the host terminal.
v0.9.3 - 2026-04-08¶
~/.local/binand~/.local/share/claudeare now read-only bind mounts instead of persistent writable overlays. Under the split-mode default introduced in v0.8.0 these host-managed tool-install directories were being treated asCategoryData, which let in-sandbox tool self-updaters (e.g. Claude Code's own updater) write partial/empty files into the per-project overlay upper-dir. Those writes shadowed the real host binaries in every subsequent session, causing failures likefish: '/home/$USER/.local/bin/claude' exists but is not an executable file(exit 126).
v0.9.2 - 2026-04-08¶
Fixed¶
- HTTP proxy no longer intercepts the body of HEAD requests. The previous behavior broke
Content-Lengthhandling and caused errors for some clients (e.g. Helm pulling OCI charts).
v0.9.1 - 2026-04-07¶
Fixed¶
- Sandbox removal now
chmods files recursively before deletion. Go populates its build cache with0500files, which previously caused sandbox cleanup to fail.
v0.9.0 - 2026-04-07¶
Added¶
devsandbox scratchpad [name] [command...]subcommand for running sandboxes in managed, clean working directories under~/.local/share/devsandbox-scratchpads/. State persists between runs. Name defaults todefault.devsandbox scratchpad listanddevsandbox scratchpad list --jsonlist scratchpads with size and state info.devsandbox scratchpad rm <name>(with--all,--keep-state,--force) removes scratchpads and their sandbox state.
Fixed¶
- Git tool now strips sensitive fields from
.git/configin place instead of replacing the file wholesale. The previous full replacement caused the git CLI to refuse to operate even for read-only commands inside the sandbox.
v0.8.2 - 2026-04-06¶
Fixed¶
- Claude tool stores project knowledge under the
datasection so chat history persists between sandbox runs.
v0.8.1 - 2026-04-06¶
Added¶
- macOS support for the devsandbox shim via a platform-specific copy-on-start overlay implementation, split from the Linux path.
jqis now included in the default Docker image.
Changed¶
- Debian base image bumped in the Docker image.
- mise-managed tool dependencies bumped.
- Docker and lint CI workflows limit concurrency to avoid redundant runs.
Fixed¶
- Restored shim source files that were missing from the v0.8.0 release and added CI coverage so the shim is built and verified on every run.
v0.8.0 - 2026-04-05¶
Breaking Changes¶
[overlay] enabledremoved - replaced by[overlay] defaultwhich accepts:split(default),overlay,tmpoverlay,readonly,readwrite.[tools.mise] writableandpersistentremoved - use[tools.mise] mount_modeinstead. Mise no longer has tool-specific overlay configuration; use the unifiedmount_modesystem.- Default mount behavior changed - tool mounts now default to
splitoverlay policy (configs → tmpoverlay, caches/data/state → persistent overlay) instead of read-only bind mounts. This prevents supply chain attacks from poisoning host tool configurations through sandboxed package managers.
Migration Guide¶
| Before | After |
|---|---|
[overlay] enabled = true |
[overlay] default = "split" (or omit - it's the default) |
[overlay] enabled = false |
[overlay] default = "readonly" |
[tools.mise] writable = true, persistent = true |
[tools.mise] mount_mode = "overlay" |
[tools.mise] writable = true, persistent = false |
[tools.mise] mount_mode = "tmpoverlay" |
| (no equivalent) | [tools.git] mount_mode = "readwrite" |
Added¶
- Binding categories - tools now classify each mount as
config,cache,data,state, orruntime, enabling differentiated overlay policies. [overlay] default- global mount mode for all tool bindings with five modes:split,overlay,tmpoverlay,readonly,readwrite.- Per-tool
mount_mode- override the global default for specific tools (e.g.,[tools.git] mount_mode = "readwrite"). Acceptsdisabledto prevent a tool's config from being mounted entirely.
Changed¶
- Tool bindings no longer hardcode
ReadOnlyorType- the builder resolves these based on the mount mode policy chain (per-tool > global > split). - Claude, Copilot, Codex, and OpenCode tools previously mounted configs read-write; they now follow the global mount mode (default: tmpoverlay for configs).