Changelog¶
Changelog¶
All notable changes to this project will be documented in this file.
Unreleased¶
Added¶
devsandbox agent-wrappers activatenow takes--agents, so you can wrap only the agents you name and leave the rest running unsandboxed:--agents claude,codex, the flag repeated once per agent, or both. Omitting it wraps every supported agent that is installed, exactly as before. An unsupported name or an explicitly empty selection exits non-zero listing the supported agents and writes nothing to stdout, so a startup file never evaluates half a snippet; selecting an agent you have not installed stays a no-op rather than an error. See Tools: Shell wrappers.
Changed¶
- Updated embedded bubblewrap to 0.12.0.
Fixed¶
- Custom mount rules now mask files reached through symlinks without bubblewrap 0.12.0 aborting with
Can't mount on symlink destination. See Custom Mounts.
v0.20.1 - 2026-08-25¶
Changed¶
readwritegit mode now mounts host files outside~/.gitconfig: the ignore and attributes files, and the[include]targets your own global config names. Marginal next to the~/.sshand~/.gnupgthis mode already carries, and it reaches only files your config already points at, but it is new host surface and worth stating.readonlyanddisabledare unchanged. One consequence to know about if your global config lives solely at~/.config/git/config: that file is now mounted read-only, like~/.gitconfigalways has been, sogit config --globalinside the sandbox fails withDevice or resource busyinstead of falling through to a throwaway~/.gitconfigthe host never sees. Usegit -c user.email=...orgit config --local. See what readwrite carries.
Fixed¶
- Git now works when you launch devsandbox from a git worktree you created yourself, rather than one made by
--worktree. A worktree's.gitis a file pointing at<main-repo>/.git/worktrees/<name>, which sits outside the project mount - so every git command in the sandbox failed withfatal: not a git repository: (null), and so did anything built on git, fromgit statusto pre-commit hooks to a test that shells out togit config. The shared git directory is now detected and mounted, read-only under the defaultreadonlygit mode and writable underreadwriteanddisabled. Note that it holds the whole repository - every branch and object, plus your other worktrees' metadata - so areadwritesandbox launched in a worktree can write all of it. Sandbox state stays keyed on the worktree path, so an existing worktree sandbox keeps its overlay. A worktree of a submodule still cannot be mounted, but now says so in a warning at launch instead of failing silently later. See Launching inside an existing worktree. - Your global git ignore and attributes rules, and every
[include]/[includeIf]block your global config pulls in, now apply inreadwritegit mode too. That mode mounts your~/.gitconfigverbatim, so its paths named host files the sandbox never had - and git ignores a missingcore.excludesFile,core.attributesFileor include target with exit 0 and no warning, which madereadonlystrictly more faithful to your host config than the mode where commits actually land. Agit statusin areadwritesandbox listed files your global ignore rules cover, and an identity kept in an[includeIf "gitdir:..."]block, or solely at~/.config/git/config, was simply absent. devsandbox now mounts the files your config names, at the paths their values spell, on every backend. Values are not rewritten and no key is dropped. Note the residual limitations on thedockerandkrunbackends, all of which come from~being/home/sandboxuserthere and none of which affectbwrap: an[includeIf "gitdir:~/..."]condition is re-evaluated inside the sandbox and stops matching, so the identity falls back to the outer config's; a~/value that climbs back out of$HOME(~/../shared/ignore) is mounted where it resolves on the host, which is still where guest git looks when the two homes share a parent and misses only when they do not; and one file included twice under two different spellings is mounted for the first spelling only. Spell such conditions and values absolutely, and use one spelling per included file. See what readwrite carries. - Commits made in a
--worktreesandbox withreadwritegit mode now land on the host. The main repository's.git, which a worktree's objects, refs and index all live in, was mounted without an explicit type and so resolved to a throwaway overlay under the defaultsplitoverlay policy - writes went to tmpfs onbwrapand to a container-local copy ondockerandkrun, and every commit was discarded at exit with no error at any point. It is now pinned to a writable bind mount, which is what the mode always promised - but only where the mount mode would otherwise have produced an overlay, so an explicitmount_mode = "readonly"on[tools.git]or a global[overlay] default = "readonly"still mounts it read-only rather than being silently overridden.readonlygit mode, which pins a read-only bind, was never affected. - A file-valued key or
[include]target read from a config file inside the main repository's.gitis no longer acted on in areadwrite--worktreelaunch. That tree is bound read-write so commits can land, which makes it sandbox-writable between launches, but it sits outside the project directory and so was missing from the roots devsandbox refuses to take such values from - letting a config rewritten in-sandbox choose which host file the next launch mounted. It is now refused like the project directory, the shared$TMPDIRand the sandbox home. A launch that mounts that tree read-only, whether throughreadonlygit mode ormount_mode = "readonly", still trusts it. - Your global git config files are now mounted read-only under every mount mode in
readwritegit mode -~/.gitconfig,~/.config/git/configand every[include]target. devsandbox resolves those files to decide which host files to mount, so withmount_mode = "readwrite"on[tools.git](or a global[overlay] default = "readwrite") they were writable host binds: a sandbox could appendcore.excludesFile = ~/.aws/credentialsto your real~/.gitconfigon one launch and have devsandbox bind that file in - writable - on the next, with both trust checks satisfied. The ignore and attributes files those configs name are not parsed by devsandbox and still follow the mount mode. Onlymount_mode = "readwrite"changes behavior here; every other mode already mounted these read-only, sogit config --globalfails withDevice or resource busyas it did before. - The resolver that reads your global git config no longer inherits
GIT_CONFIG_GLOBALfrom the host. That variable replaces the entire global scope, while git inside the sandbox reads the bound~/.gitconfigregardless - so with it set, devsandbox mounted files named by a config the sandbox never reads. Only hosts that set it are affected.
v0.20.0 - 2026-08-22¶
Breaking Changes¶
-
A
--no-mitmproxy run with apath- orurl-scoped filter rule now aborts instead of starting. Those scopes need a URL, and an HTTPS connection reaches the proxy asCONNECT host:port- so such a rule covered plain HTTP only while reading as though it covered everything. The error names the rule; rewrite it athostscope or keep MITM enabled. Runs with only host-scoped rules, and every run with MITM enabled, do not abort - but note that host-scoped rules now also apply to HTTPS on a--no-mitmrun, where they previously did not, so adefault_action = "block"allowlist starts refusing HTTPS it used to tunnel. See Filtering without MITM and the corresponding entry under Fixed. -
A
url-scoped filter orlog_skiprule written as a regex must now spell the host in lower case, with no trailing dot and no default port. The URL a rule is matched against is canonicalized, which includes dropping the port the scheme implies - sohttps://api.example.com:443/v1/.*now matches nothing, where before this release it was the only spelling that could match an intercepted HTTPS request. Exact and glob patterns are canonicalized alongside the target and need no change; regexes are left as written, because a URL's path half is case-sensitive. Check any regex rule aturlscope, including ones auto-detected as regex by a metacharacter rather than declared withtype = "regex". See Host matching is case-insensitive.
Added¶
- The repository is now a Claude Code plugin marketplace with two plugins, installed independently:
devsandbox-configanswers configuration questions from the current documentation instead of a recalled older release, anddevsandbox-triageexplains a command that failed on a sandbox restriction once, instead of letting the agent retry against the boundary. See Claude Code plugin. - New
proxy.max_log_body_bytessets how much of each request and response body the proxy records in its log (default 256 KiB,0records none). Bodies still reach their destination whole - only the logged copy is bounded. Entries whose recorded copy was cut carry a*_truncatedflag; like every other optional field in a log entry it is absent rather thanfalsewhen it does not apply, so a parser must read absence as false. See Body Capture Limit. - New
proxy.redaction.max_scan_bytessets the largest request body content redaction will hold while scanning it (default 10 MiB). A request past the limit is blocked, since a scan of part of a body proves nothing about the rest; raise the key if a workflow legitimately uploads more than that through the proxy. A project.devsandbox.tomlmay only lower it; the global config and[[include]]files, which are host-owned, may move it either way. See Redaction Coverage. - Your global git ignore and attributes files now reach the sandbox, so
git statusno longer lists files yourcore.excludesFilerules cover. In the defaultreadonlygit mode both files - whether named bycore.excludesFile/core.attributesFileor found at git's default~/.config/git/ignoreand~/.config/git/attributes- are copied in read-only and the sanitized~/.gitconfigpoints at the copies; previously they named host paths the sandbox never had, and git ignores a missing one in silence. A file devsandbox cannot carry in is named in a warning instead of being left as a value git would drop without a word. See Git.
Changed¶
devsandbox tools checkno longer reports a missing git config on a host that keeps its git configuration solely at~/.config/git/config(or$XDG_CONFIG_HOME/git/config). Every file git reads as global-scope configuration now counts, and the message names both locations. In the defaultreadonlygit mode it also lists the global ignore and attributes files it will carry into the sandbox, resolved the same way the launch resolves them - so acore.excludesFileset from an[include]is reported at the path it actually names. Those paths appear on their ownconfig:lines, and in--jsonasconfig_paths.
Fixed¶
- The sanitized
~/.gitconfigand the global ignore and attributes copies now reach the sandbox under thedockerandkrunbackends. Those backends mount the sandbox home at/home/sandboxuser, while the generated files were mounted at the host home path - so git in the guest never saw them, and it reports a missing gitconfig,excludesFileorattributesFilewith exit 0 and no warning. The destinations now follow the sandbox home wherever a backend places it, and thecore.excludesFile/core.attributesFilevalues are written~/-relative so they resolve on every backend. Thebwrapbackend, which binds the sandbox home at the host home path, is unaffected. - A kept Docker container (
keep_container) is now recreated when the mounts a tool contributes change, instead of being reused with the set baked in at creation. Volume mounts cannot be added overdocker exec, and the reuse check only looked at the tool configuration - so a mount that appeared afterwards was simply absent: the sanitized git ignore and attributes copies never reached a container created before you setcore.excludesFile, and a container created by an older devsandbox kept mounting the generated git config at the host home path. Because git ignores a missingexcludesFilewith exit 0 and no warning, this was silent. The contents of the configs devsandbox generates count too: the sanitized~/.gitconfigand the ignore and attributes copies are rewritten through a rename on every launch, so changing your host git identity or your global ignore rules put new bytes behind a mount path that never moved - and the container kept serving the file it was created with. Launching from a different terminal window still reuses the container. - The sanitized
~/.gitconfigcan no longer carry a key outside its allowlist. Values were written into the generated file verbatim, so auser.namecontaining newlines - which git preserves - opened a section of its own, and everything after it landed inside that section; the same path silently truncated an ordinary name at a#or;. Values are now written git-quoted, and one carrying a control character is dropped with a warning instead.core.excludesFileandcore.attributesFileare also ignored when they are set from an[include]whose target sits somewhere the sandbox can write - the project directory, the shared$TMPDIR, or the sandbox home a previous session wrote - and likewise when the file they name sits there. The value naming a host file to copy in has to come from the host, and so does the file. - Your global git ignore and attributes files now reach the sandbox on a host that has no global git config file at all; git honors
~/.config/git/ignorewhether or not any config file exists, but devsandbox stopped before carrying it. A copy left by an earlier launch is also removed once the setting behind it is gone, instead of staying mounted indefinitely. - A git identity defined through an
[include]or[includeIf]block now reaches the sandbox. In the defaultreadonlygit mode the sanitized~/.gitconfigwas built fromgit config --global user.name/user.emailand a scan of the top-level[user]section, neither of which expands includes - so a user who keeps a work identity in an[includeIf "gitdir:..."]block, the standard way to separate it from a personal one, got no identity at all inside the sandbox and every commit attempt failed on it. The safe copy is now generated from the fully resolved global configuration, evaluated against the project directory so the conditional branch that matched at launch is the one carried in. The key allowlist is unchanged:credential.helper,alias.*,url.*.insteadOf,http.extraHeader,sendemail.smtpPass,user.signingkeyand the include directives themselves are all still dropped. An identity kept only at~/.config/git/config, with no~/.gitconfigat all, is now carried in too, where it previously produced no safe config; and editing an included file now takes effect on the next launch instead of leaving the previous identity in place indefinitely. Resolving conditional includes means reading the configuration from inside the repository, which also reads its local config - so a repository whose own.git/configgit refuses to read no longer costs you the global one: the configuration is re-read from outside the repository, and the launch warns only when the host actually has an[includeIf]block that could not be evaluated. - The sanitized copy of a repository's
.git/configcan no longer be redirected onto an arbitrary host file. The copy is written into the sandbox home, which is bind-mounted read-write into the sandbox - so a session could leave a symlink where the next launch writes it and have the host follow it, either overwriting the file it named or mounting that file into the next session as the repository's config. The copy is now written through a temporary file and renamed into place, which replaces such a link instead of following it, and it is regenerated on every launch rather than skipped on a timestamp comparison the same link controlled. - The sandbox-aware configs devsandbox generates for starship, tmux, powerlevel10k, oh-my-zsh and the desktop portal can no longer be redirected onto an arbitrary host file, the same way the sanitized
.git/configcould. Each is written into the sandbox home, which is bind-mounted read-write into the sandbox, and none of them is pinned by a mount at its own path - so a session could leave a symlink where the next launch writes it and have the host follow it, truncating the file it named and overwriting it with the generated config.~/.flatpak-infoand the oh-my-zsh plugin file needed no configuration at all to reach; the other three needed the tool'smount_modeset todisabled. All five are now written through a temporary file and renamed into place, which replaces such a link instead of following it. - A revdiff review opens again under herdr with revdiff 0.8.23 and later. That release sends the viewer's stderr to a file so a fast-failing revdiff's error text survives the overlay closing, and the launch script shape devsandbox accepts had no place for a redirect - so every launch was refused with
launch script does not match the declared patternand no review ever appeared. The redirect is now accepted in the one spelling the launcher emits, and its target is confined to the shared temp directory exactly like the completion sentinel, since it is the same thing: a file the host opens for writing because sandboxed code named it. See Launch scripts are validated and relocated. - A herdr launch script can no longer name a program the validator never checked. A
KEY=VALprefix was accepted in quoted form whether or not the/usr/bin/envthat parses it was present - but with noenv, the shell treats a quoted'EDITOR=nvim'as the command word, not an assignment, and runs it: aPATHlookup for a file by that literal name, or a direct exec when the value carries a/, resolving against the project tree that is bind-mounted read-write. The pinned program was never reached, so the argv allowlist decided nothing. A quoted assignment now requires theenvprefix, and the bare unquoted form - the only spelling a shell reads as an assignment on its own - is unaffected. The same script's line splitting no longer treats a line of vertical tab, form feed or U+00A0 as blank: the shell's word separators are space, tab and newline only, so such a line is a second command it would look up onPATH, and skipping it hid that statement from the one-statement check while leaving its bytes in what the host runs. See Launch scripts are validated and relocated. devsandbox overlay migrate --applyno longer destroys a host directory when it fails to put a symlink in its place. The destination was cleared first and the link created after, so an interrupt during the recursive delete, or a full or read-only disk at thesymlinkcall, left the host subtree gone with nothing put back - the failure mode the file case was already hardened against. The link is now staged beside its destination and renamed into place, so the delete only runs once the replacement exists, and a symlink replacing a host file is now swapped atomically rather than unlinked first.--rmno longer deletes a--worktreecheckout while another session for the same project is still running. The state removal correctly declined and reported it, but the worktree teardown beside it ran first and unconditionally, sogit worktree remove --forcetook the directory and any uncommitted work in it regardless. Both are now driven by the same liveness answer. This also covers a docker or krun launch that lost the primary designation to a live bwrap session, which the previous check could not distinguish from a sole occupant.- A remembered ask-mode answer no longer overrides an explicit filter rule. The decision cache was consulted before any rule was evaluated and is keyed on the host alone, so approving one request that matched a narrowly-scoped
askrule and choosing to remember it installed a host-wide allow that short-circuited every laterblockfor the rest of the session - recorded in the log only ascached decision, with no sign a rule had been skipped. A rule that saysalloworblocknow wins outright; a remembered answer still stands in wherever the outcome would otherwise be a prompt. - A launch that arrives while
--rmis tearing the same sandbox down now waits for it and starts a fresh sandbox, as intended. The wait reopened the lock file on each attempt but could not recreate the directory holding it, and the teardown renames the whole sandbox root away - so every remaining attempt failed identically and the launch died after two seconds naming a missing lock file. The wait is also long enough to outlast the teardown it exists for: a--rm --worktreerun holds the sandbox acrossgit worktree remove --force, which on a checkout carrying build artifacts takes longer than the two seconds the retry allowed, so the launch aborted withfailed to acquire session lockrather than waiting. And a launch whose lock was taken in the instant between the teardown renaming the sandbox aside and releasing it no longer proceeds against a directory being deleted, which failed moments later naming a missing.primary.lock. devsandbox sandboxes prunenow reclaims the disk left behind by an interrupted removal.--rmand prune rename a sandbox aside before deleting it, and every listing path skips what is staged on purpose - so a removal killed partway through stranded the whole tree somewheresandboxes list, prune andoverlay migrate --all-sandboxesall ignored, with nothing able to report or reclaim it. Prune now lists them separately as interrupted removals and deletes them, leaving alone any whose teardown is still running. A sandbox whose own project directory begins with.removing-is also no longer hidden from the CLI entirely: staged trees now live in a directory of their own instead of sharing that namespace with real sandboxes.- The
proxy.redaction.appliedandproxy.credential.injectedaudit events now spell theirhostfield the same wayproxy.filter.decisiondoes. They read the request host directly and left it uncanonicalized, so an intercepted HTTPS request recordedapi.example.com:443on those two events againstapi.example.comon its filter decision - and any query joining audit events onhost, which is what the field is for, silently dropped them. - The kitty proxy no longer hangs on a request that asks kitty to withhold its reply.
no_responseandwait_for_child_to_exitwere forwarded unchecked while the proxy waits for exactly one response per request, so either one parked a handler and two host descriptors for the life of the process and made shutdown wait out its drain timeout. Both are now denied - kitty's own client sends them false for every command the proxy supports - and all reads carry a 30 second deadline. devsandbox logs internal --type proxynow reports a record too long to scan instead of silently returning a truncated log. In a compressed rotation the aborted scan also left the rest of the file unread.devsandbox logs proxy --lastwith a negative value now reports the bad value rather than panicking.- The directory herdr launch scripts are relocated to is now checked against the project directory as well as the sandbox home and the shared temp directory. The relocation root sits under
~/.cache, and the project tree is bind-mounted read-write at the path the host knows it by - so running devsandbox from a directory that contains that root (~/.cacheitself, say) left every relocated script writable from inside the sandbox, which is exactly the swap-after-validation window relocation exists to close. The check covers a project reached through a symlink too: devsandbox learns the directory as your shell spells it, so a project entered as~/proj-linkwhile the bind mounts the tree behind it would otherwise show no overlap with a relocation root spelled through the target. Such a session now starts with a warning and denies every herdr script launch instead; the other herdr capabilities are unaffected. See Launch scripts are validated and relocated. - A revdiff launch under herdr can no longer destroy a host file by planting a symlink where its completion sentinel is written. Confining the sentinel path to the shared temp directory bounds the name the script uses, not the file the write lands on: that directory is bind-mounted read-write at the same path on both sides, so the sandbox could leave a symlink at
<sentinel>.tmpand have the host's own shell follow it - truncating whatever it pointed at,~/.bashrcincluded, and writing the exit code there. The relocated script now runs under a shell setting that makes such a redirect refuse an existing path instead of writing through it. The script is also handed to/bin/shby absolute path rather than to whatevershthe pane'sPATHresolves, which a project-local bin directory can supply. See Launch scripts are validated and relocated. EDITOR,VISUALand theenvandshprograms a launch command names are now refused when the host's ownPATHresolves them inside the project directory or the shared temp directory. A bare name was accepted on the grounds that the host supplies the location - which stops holding the momentPATHreaches a directory the sandbox writes, and.venv/bin,node_modules/.binand abinadded by direnv or mise are all inside the project tree, bind-mounted read-write. The sandbox could plantnvimthere and name it by the bare spelling the allowlist already accepts. This holds for a value byte-identical to your own hostEDITOR/VISUALtoo: that spelling is accepted whole where the program it names resolves outside those directories, and refused where it does not, since the setting names a file and which file is decided by the samePATH. A name resolving outside those directories is unaffected; if the host resolvesshinside one of them, revdiff's kitty launch patterns are dropped and every launch is denied. Theshtest looks at the directory entryPATHfound as well as what it points at, so abin/shsymlink in the project aimed at the real/bin/shis refused rather than passing on its target - the terminal opens the link, and the sandbox can repoint it at any time before the launch. The same rule now applies to therevdiffbinary the launch patterns pin: an installation resolving inside the project directory or the shared temp directory is refused, since the pin would otherwise name a file whose bytes the sandbox chooses. Both directories are matched under the name your shell gave them and under the path that name resolves to, so a project entered through a symlink is bounded either way - the mount follows the link while devsandbox is told the link's own spelling. See The environment prefix.- The proxy log file a rotation hands to its background compressor no longer leaks into processes devsandbox starts while that compression runs. Every other log file devsandbox holds is closed automatically when it starts a process; this one was not, so anything launched during a compression inherited a writable handle on a host log outside its own mount namespace, and a share of the lock guarding it that outlived the compression.
- Two sessions for the same project no longer lose log entries to each other's rotation. A background compression held its source and its archive only within the session that started it, so the other session's pruning could unlink a half-written archive - leaving the compressor writing to a file nothing could reach and those entries gone - or delete a file a writer had taken over. Both files are now claimed for the whole compression, so at no point does the other session see the file being archived as free. See Log Rotation.
devsandbox overlay migrate --all-sandboxesno longer operates on a sandbox that is being deleted.--rmrenames a sandbox aside before removing it, and the migration enumerated the sandbox directory raw - so it could read the upper of a sandbox mid-removal, or one an interrupted removal left staged, and promote its contents to the host. It now enumerates sandboxes the same way the rest of the CLI does.- The completion sentinel a revdiff launch names is now confined to the shared temp directory devsandbox binds for it. The path was only checked for shape - any absolute path of safe characters passed - while the clause the launcher appends has the host truncate
<sentinel>.tmp, write the exit code into it and rename it over<sentinel>. Naming~/.bashrcor~/.ssh/authorized_keystherefore destroyed that file from inside the sandbox. The bound is taken from the same function that produces the bind mount, so the two cannot drift, and a launch whose directory cannot be derived is denied rather than left unbounded. See Shared temp directory. devsandbox logs internal --type proxynow shows the proxy's own diagnostic log. It never showed anything: the log was named.log.gzand written as plain text, and the reader opened it with a gzip reader and discarded the resulting error, so the command reported no entries and no problem - including for theDEVSANDBOX_DEBUGlifecycle lines whose own message points you at it. The active file is now.log, rotations are compressed to.log.gzas intended, the reader handles both, and files already on disk under the old name still read. A file that genuinely cannot be read is now named in a warning instead of being skipped silently.devsandbox overlay migrateno longer restores files a later sandbox session deleted when it replaced the directory with a socket or a FIFO. A deletion, an opaque directory and a plain file in a later session all correctly hid the earlier session's contents, but a form the migration cannot promote was skipped without hiding anything - sorm -rf dfollowed by anything creating a socket atd(a running agent's IPC socket is the case that turns up) put every file underdback on the host. See Migrating overlay data to host.- An
[[include]]file may again raiseproxy.max_log_body_bytesandproxy.redaction.max_scan_bytes. Both keys only tighten, because the project.devsandbox.tomlis writable from inside the sandbox - but the clamp was applied to every merged file, so a raise in a host-owned include was dropped with no error, which is a limit the user configured silently failing to apply. The restriction now applies to the project file alone. - A kitty launch can no longer be wrapped in a shell the sandbox supplies. Command patterns pin the program they allow to its resolved absolute path, but the wrapping
shinsh -c '<tool> ...'was matched on basename alone - and the shared temp directory is bind-mounted read-write at an identical path on host and sandbox, so an executable planted there and named by absolute path was accepted as the shell and run on the host as the invoking user, with the argv allowlist deciding nothing. Onlyshand/bin/share accepted now, both resolved by the host itself, and no command pattern falls back to basename matching any more. See Capabilities. kitty @ ls --all-env-varsis now denied. The response filter narrows the window list to windows the sandbox opened, but not what each one reports - so an owned window still returned the environment kitty launched it with, which is the host user's environment rather than the sandbox's. Requires the opt-inlist_ownedcapability to have been reachable at all. See Request options.- A herdr request can no longer name one method to the proxy and a different one to herdr. The case-variant key defence added for kitty and for herdr's parameters was missing on the herdr request envelope itself, where the method name lives - so
{"method":"pane.send_input", …, "METHOD":"ping"}was filtered as an unconditionally-allowedpingand executed aspane.send_inputwith the caller's parameters, past every validator, because an approved request is forwarded byte for byte. - Two launches started at the same moment for the same project no longer both claim its persistent sandbox state. Whether another session was running was checked well before the session lock was taken, so simultaneous bwrap launches could each conclude they were alone: both then wrote the same overlay upper and work directories instead of the second one running in concurrent mode, which is what left one session's installed tools and caches missing or half-written in the other. The answer now comes from the lock itself, so exactly one launch is primary however they interleave.
--rmalso no longer deletes sandbox state that another session is still using - the removal is skipped and reported. See Container Persistence. - Proxy request logs no longer lose entries once the log directory reaches its five-file limit. The name of a new log file was picked from how many files were present, a number pruning holds at the limit - so from then on each rotation reopened the file it had just handed to the background compressor, which deleted it moments later, and the next rotation wrote over an archive that already existed. Everything logged after that point was lost and older archives were overwritten, which is what left
devsandbox logs proxyshowing a session's early requests and nothing since. File names now keep counting up instead of being reused, and a rotation skips a name a compression still holds, moving to the next index; shutdown waits for the compressions still running. The proxy's own diagnostic log, which is never compressed, hit the same limit differently: it stopped rotating and grew as one unbounded file. See Log Rotation. devsandbox logs proxyno longer hangs on a damaged log archive. One corrupt record in a rotated.gzfile - a write cut short by a kill or a full disk is the usual source - sent the reader into an endless loop at full CPU, so the command printed nothing and had to be interrupted, and every archive after it went unread. Archives are now read a record at a time: a damaged record is skipped and the rest of the file is still shown. Entries carrying a large captured body are also no longer dropped once they pass 64 KiB, which with the defaultproxy.max_log_body_bytesan ordinary request could reach.devsandbox overlay migrateno longer restores files a later sandbox session deleted, and now plans the same operations on every run. When one session removed a directory an earlier session had written into, that directory's old contents were still listed and copied back to the host, even though the sandbox no longer showed them; the same applied to a directory the sandbox deleted and recreated. Operations were also emitted in randomized order, so a single set of sessions could delete the directory or resurrect its stale contents depending on the run. Entries hidden by a deletion are now dropped with it, and operations are ordered by path, so a directory is always created before anything inside it. Files the host already had in a directory the sandbox emptied and refilled are a separate case: they stay in place and are merged with what is promoted. See Migrating overlay data to host.devsandbox overlay migrate --applyno longer aborts when an entry changed type inside the sandbox. Replacing a host file with a directory, or a directory with a file or symlink, failed withnot a directory,file existsoris a directory- an ordinary overlay change, since deleting a name and recreating it as something else is how a file becomes a directory. The migration stopped there with the operations before it already applied, and re-running died at the same place, so the host was left half-migrated with no way to finish. The destination is now replaced, which means an overwrite can remove a host directory when the sandbox has a file or symlink at that name. The default dry-run marks each such line← replaces a host directory: its contents are deletedand counts them in its summary, so a recursive delete is visible before you pass--applyinstead of reading as an ordinary file overwrite. A destination directory the sandbox also has as a directory keeps its existing contents, as before.devsandbox overlay migrate --applycan no longer write outside the host path it was given. A symlink at a destination path was opened and truncated, so the migration rewrote whatever the link pointed at - any file on the host, named nowhere in the preview - and a symlinked directory below the target path was followed the same way. Destinations are now replaced atomically without being opened, leaving the old link's target untouched, and a symlinked parent component aborts the migration naming it. See Safety model.- The
KEY=VALprefix a kitty or herdr launch command may carry is now restricted toEDITOR,VISUALandREVDIFF_EXIT_CODE_ON_ANNOTATIONS, and their values are checked rather than passed through. Any uppercase variable was accepted with an unconstrained value, so an overlay launch could setBASH_ENVor pointEDITORat a binary planted in the shared temp directory - which the host executes as the invoking user when a file is opened in the overlay.EDITORandVISUALnow accept only a known editor - or whatever your own hostEDITOR/VISUALnames, so an unusual one keeps working - given either as a bare name or as an absolute path the host's ownPATHlookup yields; relative paths, path arguments, flags that editor does not treat as a plain switch, and shell-significant characters are refused. Naming the program alone was not enough: the file the editor opens lives in the project tree, which is bind-mounted read-write, soEDITOR=shwould have made the sandbox's own file the program. The path is compared literally rather than resolved, so a symlink in the shared temp directory pointing at the real editor cannot be repointed after the check. Theenvprogram is pinned to/usr/bin/envor the host's own resolution of it instead of matching any path ending in/env. See The environment prefix. - A kitty
lsresponse the proxy cannot filter is now refused instead of being passed to the sandbox unchanged. The filter narrows the window list to the windows the sandbox itself opened; when parsing failed it logged the error and forwarded the upstream reply anyway, which is every OS window, tab, title, working directory, foreground command line and per-window environment on the host - the opposite of what thelist_ownedcapability promises. See Request options. - The kitty proxy now checks the whole request, not just the command line.
kitty @ launchcarries around forty further options and only--typeand the argv were ever read - the rest of the request was forwarded to the host socket unchanged, so sandboxed code could pair an allowed command with--env,--watcher,--copy-cmdlineor--stdin-sourceand run its own program on the host, or read another window's screen contents. Every option is now decoded and vetted,--cwdand--matchare pinned to values devsandbox resolves on the host, and an option the proxy does not model is denied rather than passed through. The same applies tosend-text --alland--match-tab, which reached past the owned window, and to encrypted or password-authenticated requests, which hid the real command from the filter.--markerand--logoare denied with the same group: afunctionmarker spec has kitty run the named Python file, and both take an absolute path verbatim - including one in the temp directory shared read-write with the host.send-text --session-idis denied alongside--alland--match-tab. Option lists come from kitty 0.46.2; a newer kitty that adds one denies the launch naming the unknown field. See Request options. - A kitty or herdr request can no longer smuggle a second value past the field checks by spelling a key in a different case. The proxy matched JSON field names case-insensitively and kept the last of several matches, while kitty and herdr read the key they name and treat the spellings as distinct - so
{"args": ["/bin/sh", …], "ARGS": ["/usr/bin/revdiff"]}was validated as the second list and forwarded as the first, since an approved request goes to the host socket byte for byte. That defeated the argv allowlist and every pinned option, and the same trick on the envelope'scmdmade the response filter treat a host-wide window dump as a launch. Keys are now matched exactly: a case variant, a repeated key, or anything after the request object is a denial. devsandbox overlay migrateno longer stalls or dies on a socket or FIFO left in a sandbox overlay.~/.claude/channels/matrix/mux.sockis the usual one: reading a socket aborted the apply partway through and every retry died at the same entry, and a FIFO hung the command with no output at all. Both are skipped, along with device nodes - they carry no meaning on the host.- Sandbox state is no longer wiped from under a running session when the launch that started first has already exited. Owning the sandbox was decided by a lock the primary session drops on exit, so a launch arriving after that took ownership even though the earlier concurrent session was still running - deleting its overlay directories on startup and writing the layer it had mounted. Ownership now also requires that no other session holds the sandbox at all. A launch that arrives while
--rmis tearing the sandbox down waits for it instead of failing outright. See Container Persistence. - Two sessions for the same project no longer overwrite each other's proxy request log. Both write into the log directory the project's sandbox root gives them, and each appended to whichever file was newest - so each counted only its own bytes toward the rotation size, and the first to rotate handed the shared file to the background compressor, which deleted it while the other session was still writing to it. Everything that session logged from then on was lost with every write still reporting success. A writer now holds the file it is using, and the other starts its own; a file a live session holds is also left out of pruning.
devsandbox logs proxyno longer drops the rest of a file when one record is too long for it. A record past the reader's 8 MiB limit ended the scan, and everything logged after it went unread in silence - which read as an archive that simply held fewer requests. Such a record is now stepped over, counted and reported, and the entries after it are still shown. Two ways of producing one are closed as well:proxy.max_log_body_bytesis capped at 2 MiB, and the headers a log entry records are now bounded at 64 KiB per request and per response, markedreq_headers_truncated/resp_headers_truncatedwhen anything was dropped. The sandbox picks which host a request reaches, so an upstream it controls could answer with megabytes of headers - enough to make a record the reader refused, and to push real entries out of the rotation.- A large or endless upload from inside the sandbox no longer exhausts host memory or hangs the proxy on its way to being logged. Every request was logged before the filter decided on it, and the log captured the whole body with no limit and no deadline - so a request the filter was going to block could still spend unbounded host memory first, and a body that never finished sending held the connection's handler forever. The proxy now records at most
proxy.max_log_body_bytesand gives up on a stalled body after 5 seconds; entries cut short are markedreq_body_truncated/resp_body_truncatedso a truncated body is not read as a short one. Content redaction, which is off by default, reads an allowed request's body in full because scanning it for secrets requires holding all of it - now bounded byproxy.redaction.max_scan_bytesand a 30 second deadline, past which the request is blocked rather than forwarded unscanned. - Filter rules now apply to HTTPS when MITM is disabled.
--no-mitmtunneled everyCONNECTwithout consulting the filter, so adefault_action = "block"allowlist let all HTTPS through while startup warned only that filtering was "limited to host-level matching" - host-level matching was not happening either. Host-scoped rules are now evaluated per tunnel, a blocked one gets403, and ask mode prompts per tunnel with the same per-host decision cache. The startup notice about it is now informational rather than a warning, so a--no-mitmfilter run no longer pauses for confirmation - every rule that survives startup is enforced in full. See Filtering without MITM. devsandbox logs proxynow records one entry per HTTPSCONNECTin transparent mode, with the host and the filter decision. Previously the only trace was an audit event emitted once per host per session, so repeat connections and their decisions were invisible.- A forwarded port that fails to connect now reports why reliably. The helper process's error output was read while it was still being written, so the message explaining the failure could come back truncated or garbled.
- Proxy host rules now apply to every spelling of the name they cover. A rule for
blocked.example.comused to missBLOCKED.EXAMPLE.COMandblocked.example.com., which reach the same server, so a block rule could be bypassed by changing the case of a request. Host-scopedexactandglobpatterns and the request host are both lowercased and stripped of a trailing dot before matching, and a host-scopedregexis compiled case-insensitively - lowercasing a regex would change what its character classes and anchors mean, but leaving it case-sensitive against an always-lowercase target would have silently retired any host regex written with uppercase. Add(?-i)inside the pattern if you want a case-sensitive host regex.url-scoped rules canonicalize the host half of the URL the same way and leave the path alone, since a URL's path is case-sensitive and its authority is not. The same canonicalization applies tolog_skiprules, credential injectorhostpatterns, and the ask-mode decision cache - approving a host once now covers its other spellings instead of asking again. See Scopes. devsandbox overlay migrate --applyno longer deletes a host directory when it cannot write the file meant to replace it. The destination was cleared before the replacement was read, so an unreadable, swapped or oversized source - all things a sandbox-writable overlay can produce between the preview and the apply - left the host directory gone with nothing put back, on the first run and identically on every retry. The replacement is now staged in full first and the destination cleared only once it exists.devsandbox overlay migrateno longer restores files hidden by a directory a later session replaced with a file or a symlink. Deletions and emptied directories were both handled, but a directory replaced by a non-directory was not, so the earlier session's entries were still listed and the apply then died atnot a directorywith earlier operations committed, failing identically on every re-run.overlay migrate --set-modeno longer writes through a symlink at.devsandbox.toml. The project directory is bind-mounted read-write and the protective bind over that file is only applied when it already exists at launch, so a project without one let the sandbox create the name as a link to any host file, which a later--set-modewould read, rewrite as TOML and hand back - skipping the confirmation prompt entirely when only one config was affected. A non-regular file is now refused, and the write goes to the name itself.- A filter rule with
action = "ask"is now actually asked. The approval queue was built only whendefault_actionwasask, so such a rule under any other default let the request through unprompted while recording the decision asaskin the request log - an audit trail asserting an approval nobody was shown. Note the consequence: matches of such a rule now need a monitor, and a request nobody answers - or one that arrives with no monitor connected - is blocked. The startup banner advertisesdevsandbox proxy monitoronly fordefault_action = "ask", so start it yourself whenever any rule carriesaction = "ask". See Ask Mode. - Host-scoped filter rules now match the host a request is actually sent to. Under MITM the decision was made against the request's own
Host:header while the connection went to theCONNECTtarget the sandbox named, so a request to a blocked host passed every host rule simply by writing an allowed name in the header - and one ask-mode approval, cached under that same name, was reusable against any destination. Both now read the connection target, as do the ask prompt and the audit event. url-scoped filter rules now match intercepted HTTPS. The URL the proxy builds for such a request always carries the port (https://api.example.com:443/v1), so a rule written the way the documentation writes them matched nothing at all - aurl-scoped block rule was a silent no-op for every HTTPS request. A port the scheme already implies is now dropped from both the rule and the request. Aurlrule whose pattern spelled its host with any uppercase was inert for the same reason - only the request side was being lowercased - and both sides are now canonicalized together. See Scopes.- A kitty launch can no longer end at a host shell.
--holdwas classified with the cosmetic options, but kitty runs the user's shell in the window once the launched command exits - so an otherwise-allowed launch could reach an interactive shell in a window the sandbox then owns, with the argv allowlist deciding nothing. It is denied alongside--hold-after-ssh, which is the same thing for one command.--coloris restricted to the inlinename=valueform, because a spec without=is a path kitty opens and parses - a host file read the sandbox names, which is what--logowas already refused for. See Request options. - A command a kitty or herdr launch names can no longer reach a binary the sandbox planted by routing an absolute path through
... Every check that pins a program to its resolved host path collapsed..textually before comparing, while the kernel expands each symlink on the way - so a link in the shared temp directory, which is mounted read-write at an identical path on both sides, made/…/tmp/<id>/link/../../../usr/bin/nanoread as/usr/bin/nanoto the filter and resolve to the sandbox's own file on the host. The same applied to theenvprogram and to anEDITOR/VISUALpath. A path that is not already in canonical form is now refused rather than cleaned. Two quoted argv tokens written with no space between them are refused for the same class of reason: the shell joins them into one word, so the argv that was checked was not the argv that ran. EDITORandVISUALmay now carry option flags, soEDITOR="code --wait",subl -wandemacsclient -tno longer deny the launch outright. Any value with a space was refused, which is the stock setting for every editor that has to be told to block. The program is still held to the same allowlist, and each flag to the words that editor is known to treat as a self-contained switch: a charset check would have acceptednvim -u,vim -S,emacs -l,hx -corkak -e, each of which turns the file the editor opens - a file in the read-write project tree - into a program the host runs. A value byte-identical to your own hostEDITOR/VISUALis accepted whole, flags included, as long as the program it names does not resolve inside a directory the sandbox writes. See The environment prefix.devsandbox logs proxyno longer reports a corrupt archive as an empty one. A.gzfile that is not gzip at all - a truncated or overwritten rotation is the usual source - produced no entries, no error and no warning, so a file whose records were unreachable read exactly like a session that made no requests. Damage met before any record has been recovered is now reported; a malformed tail after real records still reads as the normal end of an archive a killed writer left.overlay migrate --set-modeno longer hangs on a FIFO at.devsandbox.toml, and no longer widens the file's permissions. The refusal of a non-regular file could not be reached for a FIFO, because opening one blocks until a writer appears - so the command produced no output at all rather than the error it was supposed to give. Rewriting a setting also reset the file to0644, so a config deliberately kept at0600became world-readable; an existing file now keeps its own mode.--applygained the same FIFO guard for a source file swapped between the preview and the apply.devsandbox overlay migrateno longer spends minutes building its plan for a large overlay. The time it took grew with the square of the overlay's size, so a cache directory with a hundred thousand entries left the default dry run producing no output for minutes. The plan it produces is unchanged.--rmno longer lets a launch started moments later run against a sandbox that is still being deleted. The removal held the sandbox lock but deleted the lock file along with everything else, and a waiting launch recreates that file - so it took a hold on nothing, was designated the sandbox's owner, and started up against a tree disappearing underneath it. The state is now renamed aside before it is deleted, so the waiting launch sees no sandbox at all rather than half of one.- A blocked plain-HTTP request is no longer written to the proxy request log twice, or counted twice in the request total the
session.endaudit event reports. - A project
.devsandbox.tomlcan no longer raiseproxy.max_log_body_bytes, or set it to0to stop bodies being recorded at all. That file is writable from inside the sandbox, so both directions were the sandbox choosing how much of its own traffic is kept - the same reasonproxy.redaction.max_scan_bytesalready only tightened. A project file may still lower the limit. - A
--isolation dockeror--isolation krunlaunch no longer deletes a running bwrap session's overlay directories. Stale-session cleanup was gated on a flag only bwrap launches ever set, so a container launch read itself as the sandbox's sole occupant and removed every session directory under a sandbox home it shares with the live session by project name. It now consults the session designation itself. - Sandbox-chosen argv can no longer forge lines in the kitty proxy's log. Denied and allowed launches recorded the program and arguments unquoted into a newline-terminated log, so a newline in argv wrote whole records of its own - including counterfeit
allowentries - in the audit trail of the component deciding what runs on the host. - The documentation no longer presents
.envhiding as something the sandbox always does. It is on by default and can be switched off withhide_env_files = falseor--no-hide-env, but neither appeared anywhere outside the config filedevsandbox config initgenerates - so the Security Model table read as a guarantee next to rows that were marked configurable, and a reader auditing secret handling from the docs had no reason to check a shared config or a teammate's alias for a run that exposes those files. The row is now marked configurable, the[sandbox]reference documents the key, and--verbose- undocumented in the same sweep - is described with the wrapper log it controls. See Environment Files and Sandbox Settings.
v0.19.0 - 2026-08-13¶
Breaking Changes¶
- In
[tools.<name>]and[proxy.credentials.<name>], a value of the wrong type now fails the load instead of being ignored.[tools.docker] enabled = "true"used to leave the tool off with nothing said, so the sandbox ran without something the config plainly asked for. Fix the value or drop the key - quoted booleans are the common case.devsandbox doctorreports the same error in itsconfigrow. See Values of the Wrong Type.
Added¶
- An OOM-killed sandbox is now reported instead of just disappearing: a message on stderr, a
sandbox.oomaudit event, and anoom-killedoroom-kills(N)status indevsandbox sandboxes list. Nothing to configure. Always active on docker and krun; on bwrap it needs[sandbox.resources]limits, which is what gives the sandbox a cgroup of its own - without them a SIGKILL exit is reported without claiming the OOM killer caused it. See OOM Tracking. - Config keys devsandbox does not recognize are now reported on stderr under their full dotted path, so
keep_containersinstead ofkeep_containerno longer leaves the setting quietly at its default. Covers the global config, includes,.devsandbox.toml, and the[tools.<name>]and[proxy.credentials.<name>]sections -mount_modeis now recognized only for a tool that contributes mounts, so[tools.docker]and[tools.go]report it. The keys stay ignored, but the launch pauses for the warning to be confirmed. See Unrecognized Keys.
Changed¶
- A launch that raised warnings now reprints them as one block and asks
Start the sandbox anyway? [y/N]instead of starting with them unread.--yesskips the prompt, and a launch without a terminal on both stdin and stderr prints the block and continues, so a scripted run cannot block on a question nobody sees. See Startup Warnings. - The
.devsandbox.tomltrust prompt now shows only the settings devsandbox recognizes, so an untrusted project cannot pad the approval screen with comments and invented keys that do nothing. Trust still covers the whole file by hash. A.devsandbox.tomlthat is not valid TOML is now an error rather than something to approve. See Local Config Files. - The sandbox's
$TMPDIRis now cleaned up instead of growing forever. Launching empties it when no other session for the project is live, and otherwise removes only entries untouched for 7 days. Applies whenever a tool needs a host-readable temp directory -revdiffis the only built-in case. 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. The old one is removed automatically on the next launch; nothing needs reconfiguring. - Embedded
pastaupgraded to passt2026_07_28.f8df3f1(from2026_07_16.090d739).
Fixed¶
- A launch that redirects stderr no longer hangs on a prompt the user cannot see.
devsandbox claude 2>run.logwrote the.devsandbox.tomltrust question or the mise trust question into the log file and then waited on it forever. Prompts now need a terminal on both stdin and stderr, and otherwise take their non-interactive path: the local config is skipped, and mise trust is left tomise trust. - Answering an interactive prompt no longer swallows input typed after the answer. The
.devsandbox.tomltrust prompt and the startup warning prompt read one line without buffering ahead, so the workload still receives its first input. - Fixed a plain bwrap launch logging
session.startwith no matchingsession.end, which left every such session looking unfinished in the audit trail. Exit status and signal propagation are unchanged. 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).