zed-claude-devcontainer
Health Warn
- License — License: Apache-2.0
- Description — Repository has a description
- Active repo — Last push 0 days ago
- Low visibility — Only 5 GitHub stars
Code Fail
- exec() — Shell command execution in install.sh
- rm -rf — Recursive force deletion command in install.sh
Permissions Pass
- Permissions — No dangerous permissions requested
No AI report is available for this listing yet.
A sandboxed Claude Code dev container for the Zed editor.
zed-claude-devcontainer
"Our robot overlords will use NUL-terminated strings."
-- Matthew Garrett
A sandboxed dev container for running Claude Code with bypassPermissions
enabled, from Zed or a terminal. Designed for security audits and Rust development.
About this fork
Based on trailofbits/claude-code-devcontainer (Apache
License 2.0).
[!TIP]
The upstream README covers what this one doesn't:
Colima tuning, token-based (headless) authentication, the fulldevccommand list, session sync for/insights,
file sharing, network isolation, threat model, and container details.
Changes from upstream:
- Zed: the VS Code customizations are removed, and these docs cover Zed and the terminal.
- Project path: projects are mounted at
/workspaces/<folder name>, not/workspace, so Zed shows each project's
name. postCreateCommand: uses the exec (array) form, since Zed mangles string-form lifecycle commands
(zed-industries/zed#62964); proposed upstream in
#56.- umask: fish, zsh, and interactive bash restore umask
022, because some engines startdocker execprocesses
with0000(see Known issues). - git:
~/.gitconfig.local, written bypost_install.pybecause the host~/.gitconfigis mounted read-only, setscore.checkStat=minimalandcore.trustctime=falseso bind-mounted files don't show as modified, and makes git use
theghlogin for GitHub (see GitHub access). - fish: the login shell,
$SHELL, anddevc shelldefault, with.zshrc's aliases and fzf integration indevcontainer.fishand history on the/commandhistoryvolume. Claude Code's Bash tool still uses zsh. - Tools: Neovim from GitHub releases (Ubuntu's is too old for lazy.nvim); Rust via rustup with
rustfmt,clippy,rust-analyzer, and thewasm32-wasip2target;cargo-audit,cargo-deny,cargo-semver-checks,cargo-nextest,cargo-generate,cargo-dylint, anddylint-linkvia
cargo-binstall from each project's release binaries (compiling from
crates.io as a fallback, without QuickInstall builds or telemetry); zizmor
via uv;libxdo-devandxvfbfor GUI automation and tests on a virtual display. - Dotfiles: cloned from
DOTFILES_REPOand stowed (see Dotfiles). devc rebuild --no-cache: rebuilds without Docker's build cache, still runningdevc's checks.- Schema:
devcontainer.jsonuses the base schema, avoiding avscode://reference other editors can't resolve. - CI and updates: a
buildworkflow lints the template, builds and checks the image, and runs zizmor. Renovate
also tracks the Node.js and Python series and the CI's hadolint image, with a 10-day minimum release age.
Why use this?
Running Claude with bypassPermissions on your host lets it run any command without asking. Here it can only reach
the project folder and a disposable container. Use it for security audits of client code, exploring untrusted
repositories, letting Claude change code freely, and work across several related repositories.
Prerequisites
A Docker engine: OrbStack (recommended on Apple Silicon), or the untested
Docker Desktop or Colima
(brew install colima docker && colima start).Zed, optional.
jq, which
devcuses to editdevcontainer.json(included in macOS 15 and later).Node.js and npm.
devcand the Dev Container CLI, installed once:npm install -g @devcontainers/cli git clone https://github.com/0xdea/zed-claude-devcontainer ~/.claude-devcontainer ~/.claude-devcontainer/install.sh self-install
Quick start
Set up every project with devc on the host before opening it in Zed.
Pattern A: one repository per container
Each project gets its own container and volumes. Best for one-off reviews and code you don't trust.
git clone <untrusted-repo>
cd untrusted-repo
devc . # installs .devcontainer/ and starts the container
The repository's .git/config and .git/hooks are mounted read-only, so code in the container can't plant git config
or hooks that run on your host. Git commands that write the config fail there, such as git remote add and git config
without --global. git push -u pushes but can't record the upstream branch, so set it on the host.
The container's global gitignore excludes paths such as .claude/, .vscode/, .idea/, .zed/, and *.log, so files planted
under those names don't show up in git status inside the container. Check with git status --ignored, or on the host.
Pattern B: shared workspace (not a git repository)
Several repositories share one container and its volumes. Best for related repositories you trust.
mkdir -p ~/sandbox/workspace-name
cd ~/sandbox/workspace-name
devc . # installs .devcontainer/ and starts the container
git clone <repo-1>
git clone <repo-2>
Here devc removes the template's .git mounts; without it, Zed fails withbind source path does not exist: .../.git/config. If you later run git init in the workspace, rundevc template . to restore them.
[!WARNING]
Repositories inside the workspace aren't protected: code in the container can change their.git/configand.git/hooks, which git runs on your host the next time you use it there. Use Pattern A for code you don't trust.
Opening the project
Open the project folder in Zed and choose Open in Container, or run Project: Open Remote → Connect Dev
Container from the command palette. Zed attaches to the container devc started; its terminal and language servers
run inside it. Without Zed, devc shell opens a fish shell in the container.
[!WARNING]
Only open projects whose.devcontainer/came fromdevc. If a repository ships its own,devc .asks before
replacing it; answery. Otherwise Zed uses the repository'sdevcontainer.json, which can run commands on your
host (initializeCommand), mount host folders, weaken the container's isolation, and install extensions in your
local Zed (customizations.zed.extensions, see the Zed docs).
Updating and rebuilding
Run these on the host, in the project folder. Zed never rebuilds the container by itself.
| Goal | Commands |
|---|---|
| Pick up a new version of this template | devc update, devc template ., devc rebuild |
Apply your edits to .devcontainer/, or a new CLAUDE_CODE_OAUTH_TOKEN |
devc rebuild |
| Get the latest Claude Code, Rust and cargo tools, apt packages, dotfiles | devc rebuild --no-cache |
| Get the latest Claude Code only | devc upgrade |
| Start over with empty volumes | devc destroy, then devc . |
devc updateupdatesdevcand the template in~/.claude-devcontainer; projects aren't touched.devc template .replaces the project's.devcontainer/after asking. It keeps only mounts added withdevc mount, and removes the.gitmounts again in a non-git workspace.devc rebuildrebuilds the image and recreates the container. Docker reuses cached steps that haven't changed, and
its cache is shared across projects, so steps that install the latest version of a tool only rerun with--no-cache, which is slow. Versions pinned in theDockerfile(base image, Node.js and Python series, git-delta,
fzf, Neovim, cargo-binstall) change only through Renovate.- After a rebuild, the open Zed window shows "container is not running": reopen the project with
Project: Open Remote → Connect Dev Container.
Ubuntu security fixes arrive with Renovate's base image updates and with devc rebuild --no-cache. For an urgent fix,sudo apt-get upgrade in the container works until the container is recreated.
What each command keeps
devc down and devc up stop and restart the same container. devc rebuild and devc mount create a new one.devc destroy also deletes the volumes and the image.
| What | devc down, devc up |
devc rebuild |
devc destroy |
|---|---|---|---|
Project folder on the host, including target/ |
Kept | Kept | Kept |
Volumes: shell history, Claude data, gh login |
Kept | Kept | Deleted |
Tools installed by hand (apt, cargo install, …) |
Kept | Erased | Erased |
Updates inside the container (devc upgrade, …) |
Kept | Erased | Erased |
Caches (~/.cargo/registry, Neovim plugins, …) |
Kept | Erased | Erased |
fish set -U variables |
Kept | Erased | Erased |
| Running processes (tmux, servers), iptables rules | Lost | Lost | Lost |
| The image | Kept | Rebuilt | Deleted |
| Zed's thread list | Kept | Older threads hidden | Transcripts deleted |
"Claude data" is its login, settings, sessions, and memory. post_install.py rewrites its files, such as~/.gitconfig.local, in each new container, and erased caches are downloaded again when needed. To keep a tool across
rebuilds, add it to the Dockerfile. For the hidden threads, see
Known issues.
GitHub access
Each project's gh login is stored in its ~/.config/gh volume, and git uses it for HTTPS remotes on GitHub.
[!WARNING]
Anything in the container can read and use that token, including the code you review and its dependencies. A
browser-flow login (gh auth login) or a classic token controls all your repositories, including deploy keys,
collaborators, and webhooks that keep an attacker's access after you revoke the token. Use a
fine-grained personal access token instead, or don't log in
at all in projects with code you don't trust.
gh auth logout # only if you're already logged in
gh auth login --with-token # paste the token, then press Ctrl-D
gh auth status
Recommended token settings for your own repositories:
| Setting | Value |
|---|---|
| Expiration | 90 days or less |
| Repository access | All repositories, or only the ones in this workspace |
| Actions | Read-only (for gh run list and gh run view) |
| Contents | Read and write |
| Dependabot alerts | Read-only |
| Issues | Read-only |
| Metadata | Read-only (required, added automatically) |
| Pull requests | Read and write |
| Workflows | Read and write (omit if you don't edit CI from here) |
| Administration, Deployments, Secrets, Webhooks | No access |
Without Administration, a stolen token can't create a way back in, so revoking it ends the attacker's access; the
expiration limits forgotten copies. You can change a token's repositories and permissions later. For code you don't
trust, limit it to the repositories you need and omit Workflows.
gh auth logout doesn't invalidate a token. Delete a fine-grained token in your GitHub settings. A browser-flow login
can only be revoked by removing the GitHub CLI authorization (Settings → Applications), which signs out every gh
login, including on the host.
Customizing the image
| Where you change it | Lasts until |
|---|---|
Inside the container, such as devc exec sudo apt-get install -y <package> |
The container is recreated |
The project's .devcontainer/, then devc rebuild |
The next devc template . |
| Your fork of this repository | Always |
The Dockerfile's build arguments are DOTFILES_REPO, NODE_VERSION, PYTHON_VERSION, NEOVIM_VERSION,FZF_VERSION, GIT_DELTA_VERSION, CARGO_BINSTALL_VERSION, and ZSH_IN_DOCKER_VERSION. Set them indevcontainer.json:
"build": {
"dockerfile": "Dockerfile",
"args": {
"TZ": "${localEnv:TZ:UTC}",
"DOTFILES_REPO": "https://github.com/<you>/dotfiles.git"
}
}
To customize every project, commit your changes to a fork, point devc at it, then run devc template . anddevc rebuild in each project:
git -C ~/.claude-devcontainer remote set-url origin https://github.com/<you>/zed-claude-devcontainer
devc update
Commit to the fork, not to ~/.claude-devcontainer: devc update pulls with --ff-only, which fails once the two
diverge. Merge this repository into your fork to get later changes.
Extra mounts
devc mount shares another host folder with the container. It adds the mount to devcontainer.json, wheredevc template . keeps it, and recreates the container (see What each command keeps):
devc mount ~/drop /drop # read-write, for passing files in and out
devc mount ~/ref /ref --readonly # read-only; --readonly must come last
Code in the container can change anything in a writable mount on the host: prefer --readonly, and never mount broad
folders such as your home folder. Host paths can't contain commas.
Dotfiles
The build clones DOTFILES_REPO and stows it into ~:
The repository must be cloneable over HTTPS without credentials, since the build has no SSH keys.
It must contain
fish,nvim, andtmuxpackages, laid out like your home folder (such asfish/.config/fish/config.fish); otherwise the build fails. To change the set, edit thestowline in theDockerfile.If it has no tmux config (
~/.tmux.confor~/.config/tmux/tmux.conf),post_install.pywrites a default one.An optional
claudepackage is stowed into the~/.claudevolume when the container is created. If it includessettings.json, enablebypassPermissionsthere yourself ("permissions": {"defaultMode": "bypassPermissions"}).
Asettings.jsonalready in the volume blocks stowing the package; delete it once.A plain
devc rebuildreuses the cached clone, so new commits in your dotfiles repo don't reach the image;devc rebuild --no-cachepicks them up. To apply them to the running container right away, pull inside it. This
lasts until the container is recreated. The stowed files are symlinks, so re-runstowonly for new files:git -C ~/.dotfiles pull stow --no-folding -R -d ~/.dotfiles -t ~ fish nvim tmux
Known issues
World-writable files on the host
Some Docker engines, OrbStack among them (orbstack/orbstack#2759),
start docker exec processes with umask 0000. fish and zsh restore 022 in every shell, so your terminals, Claude
Code's Bash tool (zsh), and what they run are fine; bash restores it only in interactive shells. Everything else keeps0000, such as sh -c and non-interactive bash -c (including devc exec bash -c …), and Zed's remote server (which
saves your edits) with what it launches (language servers, cargo check, git), since Zed doesn't apply the shell's
umask to it (zed-industries/zed#57185). Their files in the project
folder are world-writable, so other users on the host can modify them. Git doesn't record these permissions. To check
your engine, run docker exec <container> sh -c umask. To find and fix such files from the host (-type skips
symlinks, which chmod would follow):
find <project> \( -type f -o -type d \) -perm -0002
find <project> \( -type f -o -type d \) -perm -0002 -exec chmod go-w {} +
Agent threads disappear after a rebuild
Zed only lists threads from the current container
(zed-industries/zed#56576), so a rebuild hides earlier threads,
and opening them from Show Threads History fails silently. devc down and devc up aren't affected, and the
transcripts stay in ~/.claude. To list them again in Zed, start a thread in the new container, quit Zed, and run on
the host, replacing <folder name>:
DB="$HOME/Library/Application Support/Zed/db/0-stable/db.sqlite" # Linux: ~/.local/share/zed/db/0-stable/db.sqlite
cp "$DB" "$DB.bak"
sqlite3 "$DB" "
UPDATE sidebar_threads
SET remote_connection = (SELECT remote_connection FROM sidebar_threads
WHERE folder_paths = '/workspaces/<folder name>' ORDER BY updated_at DESC LIMIT 1)
WHERE folder_paths = '/workspaces/<folder name>';"
Or resume a thread in a terminal in the container. Claude Code's picker hides sessions started from Zed, so pass the
session ID (the transcript's file name under ~/.claude/projects/), or show them with the undocumentedCLAUDE_CODE_ENTRYPOINT (sessions started this way are tagged like Zed's):
claude --resume <session ID>
env CLAUDE_CODE_ENTRYPOINT=sdk-ts claude --resume
Zed stores the container environment in plain text
Zed's database keeps each thread's container environment, including CLAUDE_CODE_OAUTH_TOKEN and ANTHROPIC_API_KEY
when you pass them from the host.
Development
The template files are at the top of this repository, so test your working copy in a scratch folder (the devc
command uses ~/.claude-devcontainer):
mkdir -p ~/devc-test
git -C ~/devc-test init # skip to test a non-git workspace
./install.sh template ~/devc-test
cd ~/devc-test
devc up
devc shell
After a change, run ./install.sh template ~/devc-test again and devc rebuild there; to test install.sh itself,
run the working copy's script instead of devc. Clean up with devc destroy -f.
The build workflow runs on pushes and pull requests, except for changes that only touch *.md, LICENSE,.gitignore, or renovate.json. Renovate opens one grouped pull request on Mondays and lists pending updates in its
Dependency Dashboard issue; merge the pull request once build passes.
Reviews (0)
Sign in to leave a review.
Leave a reviewNo results found