Deep Agents Code is a terminal coding assistant that a developer — or a CI workflow — points at a project directory and lets drive that project: it reads and rewrites files, runs shell commands, fetches web pages, delegates to sub-agents, and extends itself with operator-configured MCP servers. Its declared safety model rests on one control, a human approval prompt in front of every side-effecting tool. Two surfaces matter most. The first is the project directory itself: the runtime samples the working directory's Makefile and file listing into the system prompt on every turn, and tool results — fetched pages, search snippets, MCP responses, command output — come back into the conversation with nothing marking them as untrusted. The second is the run mode: the approval prompt is real and well built in the interactive terminal, and effectively absent in the headless and CI paths the same binary offers.
The first thing to deal with is unattended execution. In a headless run every action other than a shell command is approved automatically, and there is no setting that puts approval back for file writes, deletes, web requests or sub-agent launches. The one control that does apply to shell commands is not a boundary: an allow-everything setting is accepted, and an ordinary allow-list is checked against the name of each command only, never its arguments — so allowing any interpreter or wrapper quietly allows everything. Put together with the untrusted content flowing into the prompt, a hostile repository can steer an unattended run into arbitrary commands on the developer's machine or the CI runner, and an attacker-controlled web page can get files rewritten. Fix the headless path first: give non-shell tools the same gate the terminal has, drop the allow-everything setting, and match commands on more than their first word.
Two further items deserve attention. The local agent-runtime server the front-end talks to runs with authentication switched off, so any other process running as the same user can read the conversation or inject instructions into it — and because sessions are checkpointed, those instructions come back when the thread resumes. That checkpoint database keeps everything the agent ever read, unencrypted, with no expiry, in a directory created without explicit permissions. Alongside these, an MCP server can currently exempt its own tools from approval by labelling them read-only, and nothing durable records what the agent did or decided — the audit stream exists but only fires for operators who have configured it, which makes any of the above hard to detect after the fact.
- Working-directory project content [PRAX-2026-08-12-004] The Makefile and file listing of whatever repository the agent was pointed at are attacker-authored content, read before any approval prompt exists.
- Local-context prompt injection [PRAX-2026-08-12-004] The detection script appends that content to the system prompt on every turn, unlabelled and in the highest-trust position in the context window.
- Agent loop and tool wiring [PRAX-2026-08-12-001] The loop cannot distinguish it from operator instruction and turns it into tool calls.
- Shell allow-list check [PRAX-2026-08-12-003] The only shell check that runs without a human matches each segment's first token, so an allow-listed interpreter carries the payload in its arguments.
- Shell execute tool [PRAX-2026-08-12-001] The command executes on the developer's machine or the CI runner with the operator's privileges.
- Outbound web [PRAX-2026-08-12-004] An attacker-controlled page or search result — content nobody vetted and nothing labels as external.
- Web tools [PRAX-2026-08-12-004] It is converted to markdown and handed back as a tool result.
- Agent loop and tool wiring [PRAX-2026-08-12-004] The tool result re-enters the context verbatim and its instructions become the loop's next tool calls.
- Filesystem tools [PRAX-2026-08-12-002] In a headless run the write, edit and delete tools are auto-approved, so the injected instruction changes files on disk with no review and no configuration that would restore one.
- Local agent-runtime server [PRAX-2026-08-12-005] The agent-runtime API runs with authentication disabled, so any same-user process that finds the ephemeral port is accepted without ever proving it is the operator.
- Agent loop and tool wiring [PRAX-2026-08-12-005] Submitted inputs enter the running thread as ordinary conversation and steer the next turns.
- Session checkpoint store [PRAX-2026-08-12-010] Those turns are checkpointed into the unencrypted thread store, so they replay into the model's context whenever the session is resumed — the poisoning outlives the process that planted it.
B1 Untrusted content entering the model context (untrusted-ingress) — 3 threats, 1 remit rules · worst: confirmed
| STRIDE | OWASP | Threat | Status |
|---|---|---|---|
| T | LLM01 | The working directory's Makefile and file listing are appended to the system prompt on every turn and tool results re-enter context verbatim, so a cloned repository or a fetched page writes into the highest-trust position in the context window with no provenance marker. | confirmed PRAX-2026-08-12-004 |
| S | LLM01 | A project-supplied sub-agent definition's markdown body becomes that sub-agent's system prompt verbatim, letting repository content impersonate operator instruction inside a delegated run. | confirmed PRAX-2026-08-12-014 |
| T | LLM01 | Nothing normalises or flags hostile Unicode in content entering the prompt, only in tool-call arguments shown at approval time — partial because the same detectors exist but are wired to the approval dialog, not the ingress path. | partial libs/code/deepagents_code/unicode_security.py:140remainder: detect_dangerous_unicode is applied to action-request arguments before approval, never to detect-script output, fetched page markdown, MCP results or memory files as they enter the context. |
B2 Model decision to a side-effecting tool (tool-invocation) — 6 threats, 9 remit rules · worst: confirmed
R-21 gap An "allow everything" allow-list setting is out of bounds.
R-22 gap Absent an allow-list, approval is required in every execution context, including non-interactive and headless/CI runs.
R-24 partial Creating, writing, editing, or deleting files MUST require human approval.
R-25 partial Outbound web actions — `fetch_url`, `http_request`, `web_search` — MUST require human approval before the request is made.
R-26 partial Delegating to a sub-agent (`task`) and launching, updating, or cancelling an async sub-agent MUST require human approval.
R-28 partial The agent MUST NOT disable, weaken, or bypass the human-approval gate on side-effecting tools on its own initiative or in response to retrieved/tool content.
R-29 gap Destructive filesystem or shell operations MUST NOT execute without either human approval or execution inside an isolated sandbox backend.
R-30 gap Untrusted project content (`Makefile`, `.env`, committed config) MUST NOT drive a side-effecting action without passing the human-approval gate
| STRIDE | OWASP | Threat | Status |
|---|---|---|---|
| E | ASI05 | Injected project content reaching the model in a headless run drives shell execution on the host through an allow-listed interpreter, because the allow-list matches the executable name and never the arguments it carries. | confirmed PRAX-2026-08-12-001 |
| E | LLM03 | In a headless run every action other than execute is auto-approved — file writes and deletes, web fetches, search and sub-agent delegation all run with no gate, and no configuration restores approval for them. | confirmed PRAX-2026-08-12-002 |
| E | LLM03 | The shell allow-list is not an execution boundary: the allow-all sentinel returns approval before any pattern check, and a restrictive list is enforced only against each segment's first token. | confirmed PRAX-2026-08-12-003 |
| T | ASI04 | An MCP server decides which of its own tools need review — a server-declared readOnlyHint with no destructive hint removes the tool from the interrupt map in Manual as well as Auto mode, and tool definitions are neither signed nor version-pinned after the trust prompt. | confirmed PRAX-2026-08-12-007 |
| R | — | The project's own threat model no longer matches the code it documents — it lists a built-in HTTP tool that does not exist and denies a URL guard that does — so the accepted-risk decisions covering this boundary rest on a stale inventory. | confirmed PRAX-2026-08-12-013 |
| I | LLM10 | A model-supplied URL — which may have come from injected content — cannot be steered at loopback, private ranges or cloud metadata: the scheme is allow-listed, the resolved address is checked, every redirect hop is re-checked, and the connection is pinned to the validated IP. | mitigated libs/code/deepagents_code/tools.py:81 |
B3 Loopback agent-runtime API (control-plane-exposure) — 2 threats, 1 remit rules · worst: confirmed
| STRIDE | OWASP | Threat | Status |
|---|---|---|---|
| S | ASI07 | The runtime server is spawned with authentication disabled, so any same-user process that finds the ephemeral port can submit inputs to the running thread, read conversation state, or push state updates; loopback binding and port randomness are the only barriers and neither is an authorization control. | confirmed PRAX-2026-08-12-005 |
| T | ASI06 | Messages injected through that unauthenticated API are indistinguishable from operator turns and are checkpointed with the rest of the thread, so they replay on resume rather than dying with the session (checked: sessions.py:285 and server.py:337 — no authentication, signing or provenance check exists on the state-update path). | potential |
B4 Conversation to the model provider (model-egress) — 2 threats, 1 remit rules · worst: potential
| STRIDE | OWASP | Threat | Status |
|---|---|---|---|
| I | LLM02 | Everything the agent has read this turn — file contents, command output, fetched pages, and any credential that appeared in them — is sent to the operator-selected provider endpoint with no redaction pass on the request (checked: agent.py:2986 and the middleware stack — the only secret-redaction regex in the package runs on hook transcripts at hooks/transcript.py:53, not on model requests). | potential |
| I | LLM02 | Tracing to LangSmith is a second copy of the same conversation, but it activates only when the operator has supplied the API key and tracing environment variables. | mitigated libs/code/deepagents_code/config.py:3105 |
B5 Memory and checkpoints re-entering the agent's reasoning (stored-state) — 2 threats, 1 remit rules · worst: confirmed
| STRIDE | OWASP | Threat | Status |
|---|---|---|---|
| T | ASI06 | Memory auto-save is on by default, so content the agent absorbed this session — including instructions read out of a repository or a fetched page — can be written into AGENTS.md and loaded back into the prompt in later sessions. | partial libs/code/deepagents_code/memory_guard.py:95remainder: the guard restores only the managed onboarding-name block; the rest of the memory file is agent-writable and re-enters the system prompt on every later session. |
| E | LLM03 | Because headless runs auto-approve file writes, the agent can rewrite its own memory and skill files during an unattended run with no operator in the loop at the moment of the write. | confirmed PRAX-2026-08-12-002 |
B6 Persisted conversation state on disk (data-at-rest) — 2 threats, 2 remit rules · worst: confirmed
R-17 gap Persisted conversation/session data and memory MUST be retained only per the operator-configured retention/purge policy, and MUST NOT be kept indefinitely by default.
| STRIDE | OWASP | Threat | Status |
|---|---|---|---|
| I | LLM02 | The checkpoint database holds full conversation state including tool arguments and results — so anything the agent read, such as a .env printed by a command, is written verbatim — into a directory created with no explicit mode, unencrypted, with no retention or purge mechanism anywhere in the session, offload or config modules. | confirmed PRAX-2026-08-12-010 |
| I | LLM02 | The neighbouring credential store in the same state directory is written atomically through an O_EXCL 0600 open with a follow-up chmod, so stored provider keys are not exposed to other local users. | mitigated libs/code/deepagents_code/auth_store.py:247 |
B7 Provider credentials in the process environment (secret-material) — 2 threats, 2 remit rules · worst: confirmed
R-14 partial The secret-bearing process environment MUST NOT be forwarded wholesale to spawned subprocesses (MCP servers, hooks, the runtime server, sandbox setup) — each child MUST receive only the environment it requires.
| STRIDE | OWASP | Threat | Status |
|---|---|---|---|
| I | ASI03 | The runtime server subprocess inherits a copy of the whole launching environment with only cloud-auth names and PYTHONPATH stripped, so every provider API key reaches it; separately an MCP stdio server's declared env is type-checked but never key-filtered, letting a trusted config set PATH, LD_PRELOAD or PYTHONPATH for that child. | confirmed PRAX-2026-08-12-006 |
| I | LLM02 | Nothing scans content the agent reads into context for credentials before it is checkpointed or sent onward, so a key printed by an allow-listed command persists in the thread (checked: hooks/transcript.py:53 redacts secret-shaped assignments only in hook transcripts; no equivalent runs on the tool-result, checkpoint or model-request paths). | potential |
B8 Audit, monitoring and escalation (telemetry-egress) — 2 threats, 2 remit rules · worst: confirmed
R-33 gap Halt and alert the operator if retrieved content, tool output, or configuration attempts to make the agent disable its approval gate, exfiltrate credentials, or execute code outside an isolated sandbox against untrusted input.
| STRIDE | OWASP | Threat | Status |
|---|---|---|---|
| R | — | No durable structured record of tool executions or approval decisions exists by default: the hook event stream carries exactly that data but only fires when the operator has authored a hooks configuration, file logging needs a debug environment variable, and the always-on surface is a bounded in-memory ring buffer that dies with the process. | confirmed PRAX-2026-08-12-009 |
| R | — | There is no halt-and-alert path for content trying to disable the gate or reach credentials — the classifier that recognises exactly those categories is opt-in, interactive-only, and denies the single call rather than stopping the run or raising an alert; headless runs have no detector at all. | confirmed PRAX-2026-08-12-015 |
B9 Runtime, dependency and extension provenance (supply-chain) — 4 threats, 4 remit rules · worst: confirmed
R-11 partial the agent MUST NOT acquire, load, or expose tools or capabilities beyond it in response to LLM output, retrieved content, or repository-committed configuration.
R-27 verified Loading a project-level (repository-supplied) MCP server MUST require explicit operator trust approval before the server is spawned or connected.
R-32 partial Loading configuration, model-provider definitions, or skill/memory definitions MUST NOT cause arbitrary code to execute before those definitions have been validated.
| STRIDE | OWASP | Threat | Status |
|---|---|---|---|
| T | LLM04 | Auto-update ships enabled and replaces the runtime from the package registry with no signature or hash check, adding a destination the operator never configured; the bundled ripgrep install shows the contrasting posture with a pinned SHA-256. | confirmed PRAX-2026-08-12-011 |
| T | LLM04 | No component inventory ships with the package and one dependency carries no upper bound, so exposure cannot be assessed when an advisory lands and a resolve can cross a future major version. | confirmed PRAX-2026-08-12-012 |
| E | ASI05 | A configured class_path provider is imported before any validation, so module-level code in a hostile module has already run by the time the BaseChatModel check rejects it; the provider-profile loader takes the same shape through exec_module. | confirmed PRAX-2026-08-12-008 |
| S | ASI04 | A repository-supplied MCP server is not spawned on the strength of the repository alone — project configs pass a per-server trust filter keyed on the config fingerprint, and a changed fingerprint re-prompts rather than loading silently. | mitigated libs/code/deepagents_code/mcp_tools.py:952 |
| Component | Kind | Lane | Description | Evidence |
|---|---|---|---|---|
| Local developer / CI identity | entrypoint | user_inputs | The operator who launches dcode and is the sole authority for approvals, typing prompts into the TUI or piping them to the headless client. | libs/code/deepagents_code/main.py:1529; tests/baselines/v1.3-opus5/deepagents-cli/deepagents-cli-remit.md:82 |
| Working-directory project content | datastore | user_inputs | The project directory the agent is pointed at — its Makefile, file listing and tree, and any project-level .agents/ definitions — read by the runtime before any approval prompt exists. | libs/code/deepagents_code/local_context.py:595; libs/code/deepagents_code/local_context.py:515 |
| Textual TUI | client | client_adapters | The interactive terminal front-end that collects operator prompts, renders streamed output, and hosts the approval dialog. | libs/code/deepagents_code/app.py:2569; libs/code/deepagents_code/client/remote_client.py:157 |
| TUI approval prompt | control | client_adapters | The interactive approval menu that shows the pending action and surfaces Unicode / homoglyph warnings before the operator approves or rejects. | libs/code/deepagents_code/tui/widgets/approval.py:87; libs/code/deepagents_code/tui/widgets/approval.py:194 |
| Headless client | client | client_adapters | The non-interactive / piped-stdin front-end used for scripting and CI, bounded by turn and time budgets. | libs/code/deepagents_code/client/non_interactive.py:1342; libs/code/deepagents_code/client/non_interactive.py:1600 |
| Headless approval resolver | control | client_adapters | The headless run's stand-in for human approval: it checks shell commands against the allow-list and auto-approves every other action. | libs/code/deepagents_code/client/non_interactive.py:831; libs/code/deepagents_code/client/non_interactive.py:897 |
| Local agent-runtime server | adapter | client_adapters | An ephemeral LangGraph dev subprocess on loopback that both front-ends drive over HTTP+SSE; spawned with authentication disabled and a copy of the launching process environment. | libs/code/deepagents_code/client/launch/server.py:32; libs/code/deepagents_code/client/launch/server.py:352 |
| Agent loop and tool wiring | orchestrator | agent_core | create_cli_agent assembles the system prompt, tool set, middleware stack, subagents and checkpointer, and is where model output becomes tool calls. | libs/code/deepagents_code/agent.py:2173; libs/code/deepagents_code/agent.py:2986 |
| HITL interrupt map | control | agent_core | The approval gate: the static map of side-effecting tools that raise an interrupt before execution, extended with non-read-only MCP tools. | libs/code/deepagents_code/agent.py:2040; libs/code/deepagents_code/agent.py:2117 |
| Shell allow-list check | control | agent_core | The only shell control that applies without a human: a per-segment first-token match against the operator's allow-list, skipped entirely under the allow-all sentinel. | libs/code/deepagents_code/config.py:3037; libs/code/deepagents_code/config.py:3063 |
| Local-context prompt injection | prompt | agent_core | Middleware that runs a bash detection script in the backend and appends the working directory's Makefile, file listing and tree to the system prompt on every model call. | libs/code/deepagents_code/local_context.py:691; libs/code/deepagents_code/local_context.py:642 |
| Auto-mode classifier | control | agent_core | An opt-in, interactive-only LLM classifier that allows or denies proposed tool calls by category and fails closed to human review. | libs/code/deepagents_code/auto_mode.py:115; libs/code/deepagents_code/auto_mode.py:129 |
| MCP readOnlyHint exemption | control | agent_core | A gate-removal path: MCP tools whose server-declared metadata says readOnlyHint without a destructive hint are skipped when the interrupt map is built. | libs/code/deepagents_code/auto_mode.py:382; libs/code/deepagents_code/agent.py:2132 |
| Sub-agent definitions | prompt | agent_core | Markdown sub-agent definitions discovered from user and project directories; the body after the YAML frontmatter becomes that sub-agent's system prompt with no content validation. | libs/code/deepagents_code/subagents.py:67; libs/code/deepagents_code/subagents.py:170 |
| Model resolution and LLM call | model | agent_core | Operator-selected provider/model resolution and instantiation, including the class_path escape hatch that imports an arbitrary module before validating it. | libs/code/deepagents_code/model_config.py:426; libs/code/deepagents_code/config.py:4444 |
| Session checkpoint store | memory | agent_core | The SQLite checkpoint database holding full conversation state — prompts, model output, tool arguments and results — so threads can be resumed. | libs/code/deepagents_code/sessions.py:285; libs/code/deepagents_code/model_config.py:524 |
| AGENTS.md memory | memory | agent_core | Persistent memory files loaded into the prompt each session and, under the default auto-save prompt, written back by the agent itself. | libs/code/deepagents_code/agent.py:2562; libs/code/deepagents_code/agent.py:2282 |
| Shell execute tool | tool | tools_mcp | The shell-command tool exposed to the model when shell is enabled; the highest-consequence capability in the inventory. | libs/code/deepagents_code/agent.py:2291; libs/code/deepagents_code/agent.py:1721 |
| Filesystem tools | tool | tools_mcp | The read/write/edit/delete/search tools over the configured backend — the family the agent uses to change files in the operator's project. | libs/code/deepagents_code/agent.py:179; libs/code/deepagents_code/agent.py:1380 |
| Web tools | tool | tools_mcp | Built-in outbound tools — web_search over Tavily and fetch_url, which converts a fetched page to markdown and returns it as a tool result. | libs/code/deepagents_code/tools.py:319; libs/code/deepagents_code/tools.py:394 |
| SSRF guard | control | tools_mcp | Scheme allow-list, private/loopback/link-local address rejection and DNS pinning applied to every URL the model supplies to fetch_url. | libs/code/deepagents_code/tools.py:81; libs/code/deepagents_code/tools.py:163 |
| MCP servers | mcp_server | tools_mcp | Operator- and project-configured MCP servers (stdio subprocess or remote HTTP/SSE), their tool discovery, and the per-server trust filter applied before connection. | libs/code/deepagents_code/mcp_tools.py:2359; libs/code/deepagents_code/mcp_tools.py:952 |
| Outbound web | external_service | external_deploy | Arbitrary HTTP/HTTPS destinations the agent fetches, plus the Tavily search API — content returned from here is attacker-influenceable. | libs/code/deepagents_code/tools.py:285; libs/code/deepagents_code/tools.py:394 |
| Repository MCP config | deploy_surface | external_deploy | Repository-supplied .mcp.json server definitions — the checked-in file here declares two remote HTTP MCP endpoints — gated by a trust prompt before any server is spawned. | .mcp.json:2; libs/code/deepagents_code/mcp_tools.py:1004 |
| GitHub Action | deploy_surface | external_deploy | The published composite action that installs dcode with uvx and runs it headlessly inside a workflow, passing the workflow's prompt and shell allow-list. | action.yml:490; action.yml:413 |
omissions: Arbitration: the findings JSON records both an LLM and an agentic code on several findings without naming a primary; where this graph must pick one it follows the KB. For the environment-forwarding finding the KB's credential/identity-scope row makes ASI03 primary and LLM02 the co-tag, so the threat is tagged ASI03 while the stored tag list leads with LLM02. For the MCP readOnlyHint finding the KB's runtime tool-definition row (rug pull / unverified tool metadata) is more specific than the missing-gate row, so it is tagged ASI04 rather than LLM03. The loopback-API finding keeps ASI07 — the KB's neighbours note assigns the channel-authentication defect there. No other stored tag disagrees with the KB. Components suspected but not modelled for want of a citation that holds in this snapshot: the remote sandbox backends (Daytona, Modal, Runloop, LangSmith, AgentCore) and the conversation offload path into them are present in the tree but no finding and no in-scope control chain attaches to them here; skills, plugins and their marketplace trust store are likewise real but were left out under the size discipline, with the MCP trust filter standing for the same operator-trust mechanism. The PyPI auto-update egress has no node of its own under the size discipline; its finding is carried as a supply-chain threat instead. All fifteen findings in the JSON are represented.
generated by: Opus 5 (1M context)