Last updated: 2026-06-02 (M3.5-ext: Linux selective per-domain network allowlist landed)
This document is the single source of truth for what DeepCode protects against, what it doesn't, and how each layer composes. If you're reviewing a PR that touches credentials, sandbox, plugin runtime, or hooks — verify it against the threat model here.
DeepCode is an LLM-driven coding assistant. The threats we care about, in decreasing order of operator severity:
| # | Threat | Severity | Where mitigated |
|---|---|---|---|
| 1 | Model exfiltrates DeepSeek API key (or other env secrets) via tool call | High | M3.5 sandbox + M5.1 env strip |
| 2 | Model writes arbitrary files outside the project (/usr/bin, /etc) |
High | M3.5 sandbox + permissions |
| 3 | Plugin (third-party code) does either #1 or #2 | High | M5.1 subprocess + (M5.1-ext) OS sandbox |
| 4 | Hook script (third-party shell snippet) does either #1 or #2 | Medium | M3.5 sandbox wraps Bash; hooks bypass when invoked via /bin/sh directly |
| 5 | Hostile settings.json field (e.g. allowRead path) injects sandbox rule |
Medium | escapeSbpl() |
| 6 | Untrusted project's AGENTS.md drives the agent into harmful action | Low | Trust store (/trust) |
| 7 | DNS exfiltration of secrets from sandboxed Bash | Partly mitigated | M3.5-ext DNS allowlist (netns.ts) — names only; raw-IP dials still pass |
First time DeepCode opens a folder, you're asked "Do you trust this
directory?". If you say no, the agent runs in a heavily restricted mode:
no exec, no writes outside the project, no bypassPermissions mode allowed.
Decisions persist in ~/.deepcode/trust.json.
Every tool call goes through:
Mode policy → Permission rules → Sandbox wrap → Exec
Modes (default | acceptEdits | plan | auto | dontAsk | bypassPermissions):
planblocks all writes and exec (read/grep/glob only).defaultprompts for risky operations.bypassPermissionsis gated behind the trust store.
Permission rules in settings.json are evaluated in order: deny > ask > allow.
4 glob patterns are supported per rule (read/write/edit/exec). See
packages/core/src/config/permissions.ts.
Bash tool invocations are wrapped under platform sandbox when
settings.sandbox.enabled is true.
macOS — sandbox-exec + SBPL profile
Profile is generated dynamically per invocation (buildMacOsProfile) and
written to $TMPDIR/deepcode-sb-*.sb. Policy:
- Default-deny on file-read, file-write, and most other operations.
- Allowed reads:
/usr,/System,/Library,/private/etc,/private/var/db,/private/var/folders(dyld closure),/bin,/sbin,/opt,/dev,~/.config,~/.npm,~/.cache. Plus user-providedfilesystem.allowReadpaths. - Path traversal: explicit
(literal "/")and(literal "/private")entries sogetcwd()and parent stats work. - Allowed writes:
/private/tmp,/private/var/folders. Plus user-providedfilesystem.allowWrite(also implicitly readable). denyRead/denyWriterules appended LAST so they override allows on overlap.- Network: default-allow unless
network.allowedDomains: [](empty array) meaning "no network". Per-domain allowlisting is NOT available on macOS (SBPL has no usable remote-host predicate here) — only on Linux (see below). - Unix sockets: blocked unless
network.allowUnixSockets: true.
Linux — bwrap argv
Generated by buildLinuxBwrapArgs:
- System read-only mounts:
/usr,/lib,/lib64,/bin,/sbin,/etc(--ro-bind-try). /proc,/dev,/tmp(tmpfs).- cwd is the only bare
--bind(rw). - Always
--unshare-pid,--unshare-ipc,--unshare-uts. --unshare-netwhennetwork.allowedDomains: [](deny-all-net).
Linux — selective per-domain allowlist (netns.ts)
When network.allowedDomains is a NON-EMPTY allowlist, BashTool runs the
command via spawnNetworkSandbox:
bwrap --unshare-net --uid 0 --gid 0gives the command its own netns; a generatedresolv.conf(→ slirp gateway10.0.2.2) is bound in.slirp4netnsprovides rootless userspace NAT (tap0) and is attached to the sandbox's netns by PID (the--uid 0mapping is what lets itsetns).- The allowlisting DNS proxy (
dns-proxy.ts) on127.0.0.1:53answers only allowlisted names and NXDOMAINs the rest;slirp4netns --disable-dnscloses the10.0.2.3bypass.
Threat model + limits: this is DNS-NAME allowlisting — a process that dials
a raw IP bypasses it (adequate for the git/npm/pip-over-https workload). It
requires binding 127.0.0.1:53 (CAP_NET_BIND_SERVICE or a relaxed
net.ipv4.ip_unprivileged_port_start). When that's unavailable, BashTool
fails closed to deny-all-net (the command runs with no network, with a note)
rather than running unrestricted. Background commands always fail closed (the
slirp helper can't safely outlive the turn).
Windows — not supported. Sandbox is a no-op (see plan §0.2).
Excluded commands — git is excluded by default. The match is on the
leading whitespace-bounded token of the user command. Pipelines starting with
an excluded command DO bypass — this is documented behavior pinned by a test,
not an oversight. (M5.2 will add per-clause analysis.)
Plugins run in their own node subprocess with:
- No host fs/net access in plugin code — all capabilities (
fs_read,fs_write,bash,fetch) flow via JSON-RPC over stdio back to the host, which applies its own mode/permission/sandbox stack. - Token-protected RPC — host generates an unguessable token per plugin spawn; every RPC from the plugin must include it.
- Env scrub —
DEEPSEEK_API_KEYandDEEPSEEK_AUTH_TOKENare stripped from the child env. Plugins cannot read DeepSeek credentials. - Hash pin — plugin code is SHA-256 hashed at install time; mismatch on load fails open (drift detection).
Acknowledged gaps:
- The subprocess isn't itself sandbox-wrapped at the OS level yet. A
malicious plugin can still exfil via DNS, can read other files the host
process can read (e.g.
~/.deepcode/credentials.json). M5.1-ext closes this by spawning the plugin undersandbox-exec/bwraptoo. - A plugin can still
process.exit(N)to crash the host's plugin pool. Host restarts on next launch.
- API key stored in
~/.deepcode/credentials.jsonwithchmod 600. apiKeyHelperfield can point at an OS keychain wrapper; output is cached for 5 min (ApiKeyHelperRefresher, configurable viaDEEPCODE_API_KEY_HELPER_TTL_MS)./doctorredacts the loaded key in its output (sk-…truncated).
The SBPL profile builder treats every user-controlled string (allowRead paths etc.) as untrusted. We:
- Escape backslash and double-quote before embedding into a quoted SBPL
subpath literal (
escapeSbpl). - Apply
(deny ...)rules AFTER(allow ...)so a deny always wins on overlap. - Test injection attempts in
packages/core/src/sandbox/attacks.test.ts— try to inject)\n(allow file-write* (subpath "/"))etc., verify the resulting profile doesn't standalone-allow root writes.
packages/core/src/sandbox/attacks.test.ts contains 17 tests:
- 6 unit-level "hostile input → safe output" tests:
- SBPL paren/quote escaping
- SBPL backslash escaping
- deny-after-allow ordering
- no implicit network when allowedDomains is empty
- no implicit file-write to /usr, /System, /Library
- 3 bwrap-arg safety tests:
- no --share-net even with a non-empty allowedDomains (connectivity comes from slirp4netns externally, never by sharing the host netns)
- only cwd is bare --bind
- always --unshare-{pid,ipc,uts}
- 4 excluded-command spoofing tests:
- prefix-only match (
gitleaks) does NOT bypass - exact match bypasses
- leading-token match bypasses
- pipeline-after-excluded bypasses (documented behavior; M5.2 hardens)
- prefix-only match (
- 2 sandbox-exec e2e (macOS, runIf the binary exists):
- block write to
/usr/local/bin/* - profile is syntactically valid (smoke)
- block write to
- bwrap e2e (Linux CI, runIf
bwrapexists —bwrap-integration.test.ts):- write inside the rw-bound cwd succeeds; write to
/etcis read-only-denied /usrreadable (sandbox is usable); deny-all-net blocks outbound- block write outside cwd (host file never created)
- write inside the rw-bound cwd succeeds; write to
- selective net allowlist e2e (Linux CI,
netns-integration.test.ts):- an allowlisted domain returns HTTP 200; a non-allowlisted domain NXDOMAINs
- the sandbox resolv.conf points at the slirp gateway
| Gap | Tracking |
|---|---|
| Raw-IP egress bypassing the DNS-name allowlist | by design (name-level); IP-level filtering TBD |
| Per-domain allowlist on macOS | SBPL has no usable remote-host predicate |
Allowlist where 127.0.0.1:53 can't be bound |
fails closed to deny-all-net (documented) |
| OS sandbox wrapping the plugin subprocess | M5.1-ext |
Pipeline analysis (git ... && rm -rf /) |
M5.2 |
| Image input prompt injection (model multimodal) | v1.1 |
| Side-channel timing leaks (e.g. via exec duration) | Out of scope |
| Local malicious binaries already on $PATH | Out of scope (assume host is trusted) |
- Do NOT open a public GitHub issue.
- Email security@.dev with reproduction steps + commit SHA.
- We aim to triage within 72 hours.