Last updated: 2026-08-01 (shared app-server trust, provenance, and thin-client capabilities)
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 | Exact-definition review + source trust; hook commands remain host code |
| 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 |
The Tauri WebView, VS Code extension, and LSP process are protocol clients, not trusted runtimes. Provider credentials, model calls, tools, permissions, hooks, MCP, and plugins stay in the CLI host or app-server. The Tauri capability set is least-privilege: file/folder selection, default HTTP(S)/mailto/tel URL opening, updater operations, and application restart. It has no Tauri filesystem plugin, shell permission, generic process-exit permission, file-reveal permission, or CSP route to the provider API. Read-only file preview and session history use explicit Rust commands that reject the backend credential path; all workspace mutations use the versioned protocol.
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/trusted-dirs.json. The CLI, desktop sidecar, VS Code, and LSP
app-server entrypoints all consult the same core trust store. Until a directory is trusted,
project/local settings cannot replace provider endpoints, model/cost policy, permissions, auto
mode, sandbox, environment, hooks, MCP, credential helpers, executable voice paths, worktree/update
policy, or status-line commands. config/diagnostics reports which fields were gated without
returning their values.
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:
- Gated host capabilities — declared
fs_read,fs_write,bash, andfetchrequests flow via JSON-RPC over stdio back to the host, which applies mode, permission, hook, approval, and sandbox policy. This controls the supported plugin API; it is not yet an OS boundary against a malicious plugin using Node APIs directly. - 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. - Whole-install hash pin — every installed plugin file contributes to the SHA-256 trust hash; missing trust or any drift disables the plugin and reports a diagnostic.
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 and the platform integration suites cover:
- Unit-level "hostile input → safe output" cases:
- 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
- bwrap-argument safety cases:
- 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}
- Excluded-command spoofing cases:
- 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 (
- 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.