# Devcontainer Parity: Lessons from anthropics/claude-code (https://jackin.tailrocks.com/reference/research/runtime/devcontainer-parity/)



**Status**: Proposed — research captured, no implementation committed

**Host-affordance note**: the construct image now includes `xclip`, `xauth`, and `wl-clipboard` because Amp and other Linux CLIs expect standard clipboard helper binaries. These packages do not make a macOS host clipboard available inside the container by themselves; host clipboard image paste remains part of the [Host Affordance Bridge](/roadmap/clipboard-image-bridge/) and requires an explicit host-side bridge.

## Problem [#problem]

The upstream [Claude Code repository](https://github.com/anthropics/claude-code/tree/main/.devcontainer)
ships a working VS Code devcontainer that runs Claude Code inside an isolated
Docker environment. Its design predates jackin❯, solves several problems
jackin❯ also has, and makes a few different choices that are worth comparing
directly:

* upstream bakes a network-egress allowlist into the container image via
  `iptables` + `ipset` and a `postStartCommand` that runs it with
  `NET_ADMIN`/`NET_RAW` capabilities
* upstream grants `sudo` **only** to a single setup script, not to the whole
  session
* upstream is itself a devcontainer spec, so it works in VS Code, Cursor, and
  GitHub Codespaces out of the box
* upstream persists shell history and the `~/.claude` directory in named
  Docker volumes parameterized by `devcontainerId`

jackin❯ already does things the upstream devcontainer can't — Docker-in-Docker,
per-agent networks, workspace mounting, namespaced role repos, DinD TLS,
plugin marketplaces, construct/derived-layer split — but several tactical
pieces of the upstream setup are genuinely useful and map directly onto
existing open items ([network policy controls](/roadmap/) and
[construct user creation](/reference/runtime/user-identity/)
in particular).

This page captures the comparison so those lessons aren't lost.

## Why It Matters [#why-it-matters]

* **Network egress is currently unrestricted inside jackin❯ agents.** The
  [network egress policy](/roadmap/network-egress-policy/) roadmap
  item now owns this product gap; the upstream `init-firewall.sh` remains a
  ready-made reference implementation with a sensible default allowlist.
* **jackin❯ images are not usable as devcontainers.** Operators who prefer VS
  Code / Cursor / Codespaces for code navigation cannot point those tools at a
  jackin-generated image without authoring their own `devcontainer.json`.
  Shipping (or generating) one would widen jackin❯ surface without changing
  the runtime.
* **The construct's `claude` user has blanket `NOPASSWD:ALL` sudo.** Upstream
  restricts sudo to a single script (`init-firewall.sh`). jackin❯ model is
  intentionally more permissive because the agent runs under
  `--dangerously-skip-permissions`, but scoping sudo is still worth a design
  pass — especially if a firewall-init hook lands.
* **Shell history is not persisted across agent recreations.** Upstream mounts
  a named volume at `/commandhistory` and redirects `HISTFILE` there.

## Current State [#current-state]

### jackin❯ construct [#jackin-construct]

Source: <RepoFile path="docker/construct/Dockerfile">docker/construct/Dockerfile</RepoFile>

* `FROM debian:trixie-20260421@sha256:...` — pinned by digest
* Installs: `bash`, `ca-certificates`, `curl`, `fd-find`, `fzf`, `git`,
  `git-lfs`, `jq`, `openssh-client`, `ripgrep`, `sudo`, `tree`, `yq`, `zsh`
* Pulls in `mise`, Docker CLI + Compose plugin, GitHub CLI
* Builds and copies `tirith` and `shellfirm` binaries from a Rust builder stage
* Creates `claude` user (UID/GID 1000) with `claude ALL=(ALL) NOPASSWD:ALL`
* oh-my-zsh + zsh-autosuggestions + starship
* Zshrc enables `tirith` and `shellfirm` shell hooks

### jackin❯ runtime [#jackin-runtime]

Source: <RepoFile path="crates/jackin-capsule/src/runtime_setup.rs">crates/jackin-capsule/src/runtime\_setup.rs</RepoFile> and <RepoFile path="docker/runtime/entrypoint.sh">docker/runtime/entrypoint.sh</RepoFile>

* `jackin-capsule runtime-setup` performs git identity / GitHub setup, per-agent durable-home seeding, auth handoff refreshes, and Claude `tirith` / `shellfirm` MCP registration.
* The shell entrypoint dispatches on `JACKIN_AGENT`, runs optional runtime hooks under `/jackin/runtime/hooks/` (`setup-once.sh`, `source.sh`, `preflight.sh`), and then `exec`s the selected agent.

### jackin❯ agent layer (e.g. `jackin-agent-smith`, `jackin-the-architect`) [#jackin-agent-layer-eg-jackin-agent-smith-jackin-the-architect]

* Final stage must be `FROM projectjackin/construct:trixie` (validated contract)
* Agents layer in their own toolchains via `mise` (Rust for the-architect,
  Node LTS for agent-smith, plus OpenTofu for the-architect)
* Plugins declared in `jackin.role.toml`, installed in the derived image

### Upstream Claude Code devcontainer [#upstream-claude-code-devcontainer]

* `FROM node:20`
* Installs: `git`, `zsh`, `vim`, `nano`, `jq`, `iptables`, `ipset`,
  `dnsutils`, `gh`, `fzf`, `man-db`
* Non-root `node` user; owns `/workspace` and `/home/node/.claude`;
  `sudo` scoped to a single command: `/usr/local/bin/init-firewall.sh`
* zsh + powerline10k, git-delta 0.18.2
* Claude Code installed globally via `npm i -g @anthropic-ai/claude-code`
* `devcontainer.json`:
  * `runArgs: ["--cap-add=NET_ADMIN", "--cap-add=NET_RAW"]`
  * Named volumes per `devcontainerId`:
    * `claude-code-bashhistory-${devcontainerId}` → `/commandhistory`
    * `claude-code-config-${devcontainerId}` → `/home/node/.claude`
  * `postStartCommand: "sudo /usr/local/bin/init-firewall.sh"`
  * `waitFor: "postStartCommand"`
  * Extensions: `anthropic.claude-code`, ESLint, Prettier, GitLens
  * Env: `NODE_OPTIONS=--max-old-space-size=4096`
* `init-firewall.sh`:
  * Default DROP on INPUT, FORWARD, OUTPUT
  * Allows loopback, established/related, DNS (53), SSH (22), host subnet
  * `ipset allowed-domains` (hash:net) seeded from:
    * GitHub IP meta ranges (aggregated from `api.github.com/meta`)
    * `registry.npmjs.org`
    * `api.anthropic.com`
    * `sentry.io`, `statsig.anthropic.com`, `statsig.com`
    * `marketplace.visualstudio.com`, `vscode.blob.core.windows.net`,
      `update.code.visualstudio.com`
  * Self-test: `example.com` must fail, `api.github.com` must succeed

## Side-by-Side [#side-by-side]

| Aspect                    | Upstream devcontainer                             | jackin❯ construct + agent                                 |
| ------------------------- | ------------------------------------------------- | --------------------------------------------------------- |
| Base image                | `node:20`                                         | `debian:trixie` (pinned by digest)                        |
| Language runtime in base  | Node 20 baked in                                  | None; agents add toolchains via `mise`                    |
| Non-root user             | `node` (UID 1000); sudo scoped to firewall script | `claude` (UID/GID remapped to host); `NOPASSWD:ALL`       |
| Shell                     | zsh + powerline10k                                | zsh + oh-my-zsh + zsh-autosuggestions + starship          |
| Diff viewer               | git-delta 0.18.2                                  | none in construct                                         |
| Search / fuzzy tools      | `fzf`                                             | `fzf`, `fd-find`, `ripgrep`                               |
| Security tools            | none                                              | `tirith`, `shellfirm` (MCP + shell hooks)                 |
| Docker-in-sandbox         | not available                                     | DinD sidecar, per-agent network, TLS                      |
| Network egress policy     | iptables/ipset allowlist via `postStartCommand`   | none at container layer                                   |
| Devcontainer spec         | yes (is the product)                              | no — images not discoverable by VS Code/Cursor/Codespaces |
| Shell history persistence | named volume per `devcontainerId`                 | not persisted (inside container layer)                    |
| Claude config persistence | named volume `claude-code-config-*`               | host bind mount `~/.jackin/data/{name}/.claude`           |
| GitHub auth persistence   | not persisted explicitly                          | host bind mount `~/.jackin/data/{name}/.config/gh`        |
| Plugin marketplace        | none                                              | `jackin.role.toml` + derived-image install commands       |
| Multi-agent               | single environment                                | namespaced agents, per-workspace selection                |

## Findings [#findings]

### Finding 1 — Egress allowlist for agent containers [#finding-1--egress-allowlist-for-agent-containers]

**Gap**: Agents have unrestricted outbound network access by default. A
compromised plugin, malicious dependency, or prompt-injected instruction can
talk to any endpoint on the internet.

**Reference implementation**: upstream `init-firewall.sh` is self-contained,
\~80 lines, and covers the 90% case (GitHub, npm, Anthropic, statsig, VS Code
update endpoints). Capabilities required: `--cap-add=NET_ADMIN`,
`--cap-add=NET_RAW`. Packages required: `iptables`, `ipset`, `dnsutils`,
`curl`, `jq`.

**Options**:

1. **Port upstream script as an opt-in runtime hook.** Land the script under
   `docker/runtime/` (or `docker/construct/` so it's available to every
   agent) and wire a new env var like `JACKIN_ENABLE_EGRESS_ALLOWLIST=1` that
   flips the entrypoint to run it. Zero default behavior change; operators
   opt in per launch.
2. **Make it a per-agent declaration.** Add a `[network]` section to
   `jackin.role.toml` where agent authors can declare the required egress
   list. jackin merges the declared list with an operator-level allowlist in
   `config.toml`. Cost: schema + merge logic.
3. **Make it a per-workspace declaration.** Same mechanism but keyed on
   workspace instead of agent. Closer to how Docker Sandboxes scopes network
   policy.
4. **Host-side proxy** (further out). Intercept outbound HTTPS at the DinD
   network boundary instead of inside the guest. This is the Docker Sandboxes
   approach and closes the credential-injection gap too, but it is a
   significantly larger project. Covered in the
   [selectable sandbox backends](/reference/research/security/sandbox-backends/selectable-sandbox-backends/)
   design.

**Recommendation**: Start with Option 1 as a single-ship feature, then
promote to Option 2 once the shape settles. Option 4 belongs to the larger
microvm track.

### Finding 2 — Generate a `devcontainer.json` alongside the derived image [#finding-2--generate-a-devcontainerjson-alongside-the-derived-image]

**Gap**: jackin builds and runs the image itself; there is no way to point VS
Code, Cursor, or Codespaces at the same image for IDE features like Go-to-
Definition, debugging, or extension auto-install.

**Options**:

1. **Emit `devcontainer.json` when jackin builds the derived image.** Write
   it into the workspace's `.devcontainer/` directory (or a jackin-managed
   shadow dir) so IDEs can consume it. Fields: build target (use the jackin
   image directly), `runArgs`, mounts matching what jackin would mount,
   `postStartCommand` for per-start jackin❯ runtime hooks, `postCreateCommand`
   only for one-time initialization, an extensions list curated per agent.
2. **Ship a static reference `devcontainer.json` in the docs.** Operators
   copy it into their project. Zero product change, lowest cost. Loses
   coupling to jackin❯ actual runtime config.
3. **Skip IDE integration.** jackin stays a terminal-first product. Users
   who want IDE integration use the standalone upstream devcontainer.

**Recommendation**: Option 1 is the most aligned with jackin❯ positioning
as a managed runtime, but it is not urgent. Option 2 is a cheap way to
validate demand first.

Generated `devcontainer.json` should use `postStartCommand` for per-start
runtime setup that must rerun when the container restarts, and reserve
`postCreateCommand` for one-time image/container initialization. It should set
`waitFor` to the command that gates a usable agent environment so VS Code and
Cursor do not attach before firewall setup, mounted homes, and runtime
bootstrap are ready. The parity check should compare jackin launch readiness
against devcontainer `waitFor` semantics, not only generated fields.

### Finding 3 — Scope the `claude` user's sudo [#finding-3--scope-the-claude-users-sudo]

**Gap**: The `claude` user has `NOPASSWD:ALL`. Upstream scopes sudo to one
explicit command (`/usr/local/bin/init-firewall.sh`). jackin❯ choice is
defensible because the agent runs under `--dangerously-skip-permissions` and
is already free to do anything inside the container — but if Finding 1
lands, a firewall-init script is a new reason to allow sudo for one
specific command rather than globally.

**Options**:

1. **Leave as-is.** Tracks jackin❯ "agent is fully autonomous inside the
   box" model.
2. **Scope sudo to a curated allowlist** (`apt-get`, `init-firewall.sh`,
   and a small handful of others). Breaks agents that expect to install
   arbitrary system packages at runtime.
3. **Scope sudo only to firewall init** (matches upstream). Most restrictive;
   likely breaks current agents.

**Recommendation**: Defer until Finding 1 is implemented. Revisit as part of
a broader hardening pass and document the tradeoff in
[Security model](/guides/security-model/).

### Finding 4 — Persist shell history [#finding-4--persist-shell-history]

**Gap**: `~/.zsh_history` lives in the container's writable layer and is lost when the agent is recreated. Upstream mounts a named volume at `/commandhistory` and points `HISTFILE` there.

**This is lower priority than agent conversation history.** The upstream devcontainer conflates two things:

* **Shell command history** (`~/.zsh_history`) — what the operator or agent typed at a zsh prompt. Low value: the agent's prompts are driven by the conversation, not typed manually; the operator rarely drops into the container shell.
* **Agent conversation history** — every prompt the operator sent to Claude/Codex/Amp/Kimi/OpenCode and every answer the agent gave back. High value: this is the actual work record.

jackin❯ already preserves agent conversation history through the per-agent home bind mounts (`~/.jackin/data/{name}/home/.claude`, `.codex`, etc.) — the conversation files are on the host and survive eject. The deeper gap — cross-agent browsing, surviving `prune`, seeing history across instances — is tracked under the [Déjà Vu](/reference/research/agent-orchestration/conversation-capture/deja-vu/) roadmap item, not here.

**Shell history persistence** (the literal zsh `HISTFILE`) is a low-effort quality-of-life improvement independent of the hardening contract:

1. **Bind mount `~/.jackin/data/{name}/.zsh_history` to `/home/agent/.zsh_history`** via a single line in <RepoFile path="crates/jackin-runtime/src/runtime/launch.rs" />.
2. Or redirect `HISTFILE` in `zshrc` to a path already bind-mounted (e.g. under `/jackin/state/`).

**Recommendation**: Defer until after Déjà Vu V1. A bound `HISTFILE` adds minimal value if the operator never manually shells into the container. Track it as a small quality-of-life item. The [Déjà Vu](/reference/research/agent-orchestration/conversation-capture/deja-vu/) item is the correct home for everything worth keeping from an agent session.

### Finding 5 — Small construct additions worth considering [#finding-5--small-construct-additions-worth-considering]

None of these are gating; they're quality-of-life notes.

* **`git-delta`**: upstream uses it; would improve the default `git diff`
  experience inside agents. Low cost.
* **`man-db`**: useful when the agent or operator looks up tool docs. Low
  cost but meaningful disk impact (`man-db` + pages).
* **`vim`, `nano`**: jackin❯ construct currently has neither. Operators
  dropping into a running container to inspect state have no editor.
  Installing at least one is a small ergonomics win.
* **Deliberate omissions**: upstream installs none of `tirith`, `shellfirm`,
  `ripgrep`, `fd-find`, `yq`, `git-lfs`, `tree`, Docker CLI, `mise`,
  GitHub CLI. jackin❯ adds all of these on purpose. No change recommended —
  they are part of what makes jackin❯ baseline more useful than a bare
  devcontainer.

### Finding 6 — VS Code extension kit [#finding-6--vs-code-extension-kit]

**Gap**: upstream bundles `anthropic.claude-code`, ESLint, Prettier, and
GitLens extensions in `devcontainer.json`. Any operator who wants the same
kit under jackin❯ has to configure it themselves.

This is downstream of Finding 2 — if jackin❯ emits a `devcontainer.json`, the
extensions list becomes an obvious payload for it.

## Connection to the Docker Runtime Hardening Contract [#connection-to-the-docker-runtime-hardening-contract]

The findings here are inputs to the broader [Docker Runtime Hardening Contract](/roadmap/docker-runtime-hardening-contract/). Specifically:

* **Finding 1 (iptables egress allowlist)** maps to the `api_only` network tier in the capability taxonomy. The upstream `init-firewall.sh` is the V1 reference implementation. In jackin❯, this tier is configurable: the operator assembles the allowlist from agent endpoints + explicit additions rather than using a hard-coded Anthropic-specific list. The `NET_ADMIN`/`NET_RAW` capabilities required for iptables are implicitly granted when `network = "api_only"` is selected.
* **Finding 3 (scoped sudo)** maps to the `agent_scoped_sudo` user privilege tier in the capability taxonomy — a tier between "no sudo" and "full sudo" where sudo is permitted only for named commands. `network = "api_only"` implicitly selects this tier so the firewall init script can run.
* **Finding 4 (shell history vs. conversation history)** — the upstream conflates two concerns. Shell history (`~/.zsh_history`) is a trivial bind-mount addition, deferred. Agent conversation history — the actual work record — is already partially preserved by the per-agent home bind mounts (`~/.claude`, `.codex`, etc.) and is tracked in depth under [Déjà Vu](/reference/research/agent-orchestration/conversation-capture/deja-vu/), which covers cross-agent browsing, surviving `prune`, and the full extraction/normalization story. Both are independent of the hardening contract.

The key distinction: the upstream devcontainer makes these choices as a fixed configuration for a single use case. jackin❯ makes them configurable: every upstream choice becomes a toggle, and the session contract shows the operator exactly what was granted.

## Non-Goals [#non-goals]

* Replacing jackin❯ construct/derived-layer model with a monolithic Node-
  based image. The split is one of jackin❯ core advantages.
* Bundling IDE-specific logic into the core CLI beyond a generated
  `devcontainer.json`.
* Claiming parity with Docker Sandboxes' credential-injection model. That
  work is tracked under
  [selectable sandbox backends](/reference/research/security/sandbox-backends/selectable-sandbox-backends/).

## Related Files [#related-files]

* <RepoFile path="docker/construct/Dockerfile">docker/construct/Dockerfile</RepoFile> — base image, user creation, shell setup
* <RepoFile path="docker/construct/zshrc">docker/construct/zshrc</RepoFile> — shell hooks (touched by Finding 4)
* <RepoFile path="crates/jackin-capsule/src/runtime_setup.rs">crates/jackin-capsule/src/runtime\_setup.rs</RepoFile> — in-container Rust runtime setup (touched by Finding 1)
* <RepoFile path="docker/runtime/entrypoint.sh">docker/runtime/entrypoint.sh</RepoFile> — shell-native runtime hook sourcing and final agent exec (touched by Finding 1)
* <RepoFile path="crates/jackin-image/src/derived_image.rs">crates/jackin-image/src/derived\_image.rs</RepoFile> — derived image generation and Claude plugin install commands
* <RepoFile path="crates/jackin-runtime/src/runtime/launch.rs">crates/jackin-runtime/src/runtime/launch.rs</RepoFile> — mount and capability wiring (touched by Findings 1 and 4)
* <RepoFile path="crates/jackin-image/src/derived_image.rs">crates/jackin-image/src/derived\_image.rs</RepoFile> — derived Dockerfile generation (touched by Finding 2)
* [Security model](/guides/security-model/) — security posture (touched by Findings 1 and 3)
* Upstream reference: [`anthropics/claude-code/.devcontainer`](https://github.com/anthropics/claude-code/tree/main/.devcontainer)
