# Defense-in-Depth Score: OpenHands

**Repo:** https://github.com/OpenHands/software-agent-sdk · **Commit:** `84d4470acd85688b52c510532ffc83b234ebf453` · **Reviewed:** 2026-10-03
**What it is:** Software Agent SDK and agent server that power the OpenHands autonomous coding agent (CLI, GUI and Cloud).
**Category:** Coding
**Scored configuration:** The README quick start: Agent with the terminal, file editor and task tracker tools, Conversation(agent, workspace=cwd) on a local workspace, with constructor defaults (NeverConfirm, no security analyzer, no persistence_dir, 500 iterations, no budget).
**Agent surface (default):** code execution yes · filesystem write yes · network egress yes · external credentials yes · persistent memory opt-in · untrusted input yes · third party extensions opt-in · sub agents opt-in · external communication yes

## Score: 2.7 / 10.0 (Minimal)

| # | Criterion | S | C | D | B | Raw | Cap | Score | Confidence |
|---|---|---|---|---|---|---|---|---|---|
| C1 | Identity & least privilege | L0 | L1 | L0 | L0 | 0.07 | — | **0.07** | High |
| C2 | Approval gates | L3 | L2 | L0 | L1 | 0.42 | G1 | **0.42** | High |
| C3 | Tool & action scoping | L0 | L1 | L1 | L0 | 0.12 | — | **0.12** | High |
| C4 | Code-execution isolation | L2 | L3 | L0 | L1 | 0.42 | G1 | **0.42** (alt) | High |
| C5 | Untrusted input blast radius | L0 | L0 | L0 | L0 | 0.00 | C5-WORSTCASE | **0.00** | High |
| C6 | Memory, context & configuration integrity | L0 | L0 | L1 | L3 | 0.20 | — | **0.20** | High |
| C7 | Third-party extensions | L1 | L1 | L2 | L1 | 0.30 | — | **0.30** | High |
| C8 | Secrets & sensitive-data protection | L2 | L2 | L2 | L0 | 0.40 | — | **0.40** | High |
| C9 | Audit & traceability | L2 | L2 | L0 | L2 | 0.40 | G1 | **0.40** | High |
| C10 | Limits & kill switch | L2 | L1 | L1 | L1 | 0.33 | — | **0.33** | High |


As shipped, the SDK runs model-chosen shell commands and file edits directly on your machine, as you, with no approval step: the confirmation policy defaults to NeverConfirm. Every command inherits your full environment, including the LLM key and any cloud or GitHub tokens, so a prompt injection in a repository file can leak credentials and take irreversible actions unattended. Strong building blocks exist (per-action confirmation, deterministic risk analyzers, secret masking, a Docker workspace), but all are opt-in.

## Critical gaps
- A hijacked agent holds the user's full local authority: terminal subprocesses inherit the whole host environment and run as the OS user. (ASI03, T3; C1) — [openhands-sdk/openhands/sdk/utils/command.py:53](https://github.com/OpenHands/software-agent-sdk/blob/84d4470acd85688b52c510532ffc83b234ebf453/openhands-sdk/openhands/sdk/utils/command.py#L53); [openhands-tools/openhands/tools/terminal/env.py:34](https://github.com/OpenHands/software-agent-sdk/blob/84d4470acd85688b52c510532ffc83b234ebf453/openhands-tools/openhands/tools/terminal/env.py#L34)
- Default execution is on the host with no sandbox; the Docker workspace is opt-in. (ASI05, T11; C4) — [openhands-sdk/openhands/sdk/conversation/impl/local_conversation.py:380](https://github.com/OpenHands/software-agent-sdk/blob/84d4470acd85688b52c510532ffc83b234ebf453/openhands-sdk/openhands/sdk/conversation/impl/local_conversation.py#L380); [openhands-tools/openhands/tools/terminal/terminal/subprocess_terminal.py:161](https://github.com/OpenHands/software-agent-sdk/blob/84d4470acd85688b52c510532ffc83b234ebf453/openhands-tools/openhands/tools/terminal/terminal/subprocess_terminal.py#L161)
- With NeverConfirm as the default, a prompt-injected agent can exfiltrate environment secrets over the shell's network and take irreversible actions without any human involvement. (ASI01, T6, LLM01; C5) — [openhands-sdk/openhands/sdk/conversation/state.py:123](https://github.com/OpenHands/software-agent-sdk/blob/84d4470acd85688b52c510532ffc83b234ebf453/openhands-sdk/openhands/sdk/conversation/state.py#L123); [openhands-sdk/openhands/sdk/utils/command.py:53](https://github.com/OpenHands/software-agent-sdk/blob/84d4470acd85688b52c510532ffc83b234ebf453/openhands-sdk/openhands/sdk/utils/command.py#L53)

## Criterion details

### C1 Identity & least privilege — 0.07 (high)

The agent runs with whatever authority the person or process that launched it has. Every shell command the agent runs inherits the full environment of the host process, minus two agent-server session keys, so any cloud, GitHub, or LLM API credentials exported in that environment are available to the model's commands. There is no scoped identity and no authorization check between the model and its tools. A hijacked agent therefore holds the user's entire local account.

- **S L0:** The SDK has no agent identity of its own; terminal subprocesses get a copy of os.environ with only SESSION_API_KEY, OH_SECRET_KEY and OH_SESSION_API_KEYS_* removed. — [openhands-sdk/openhands/sdk/utils/command.py:53](https://github.com/OpenHands/software-agent-sdk/blob/84d4470acd85688b52c510532ffc83b234ebf453/openhands-sdk/openhands/sdk/utils/command.py#L53); [openhands-sdk/openhands/sdk/utils/command.py:25](https://github.com/OpenHands/software-agent-sdk/blob/84d4470acd85688b52c510532ffc83b234ebf453/openhands-sdk/openhands/sdk/utils/command.py#L25) (verified)
  - *To reach the next level:* No scoped or dedicated credentials; L1 needs at least a dedicated identity distinct from the operator's ambient credentials.
- **C L1:** The ambient environment is passed to every terminal subprocess (both tmux and subprocess backends call build_terminal_env -> sanitized_env), and there is no authorization layer in front of any tool. — [openhands-tools/openhands/tools/terminal/env.py:34](https://github.com/OpenHands/software-agent-sdk/blob/84d4470acd85688b52c510532ffc83b234ebf453/openhands-tools/openhands/tools/terminal/env.py#L34); [openhands-tools/openhands/tools/terminal/terminal/tmux_terminal.py:120](https://github.com/OpenHands/software-agent-sdk/blob/84d4470acd85688b52c510532ffc83b234ebf453/openhands-tools/openhands/tools/terminal/terminal/tmux_terminal.py#L120) (verified)
  - *To reach the next level:* No authorization layer in code for any tool; L2 needs all built-in tools to act through a scoped identity.
- **D L0:** The README default passes a workspace path, which becomes a LocalWorkspace operating directly on the host as the OS user, with the LLM key read from the environment. — [openhands-sdk/openhands/sdk/conversation/impl/local_conversation.py:380](https://github.com/OpenHands/software-agent-sdk/blob/84d4470acd85688b52c510532ffc83b234ebf453/openhands-sdk/openhands/sdk/conversation/impl/local_conversation.py#L380); [openhands-sdk/openhands/sdk/workspace/local.py:18](https://github.com/OpenHands/software-agent-sdk/blob/84d4470acd85688b52c510532ffc83b234ebf453/openhands-sdk/openhands/sdk/workspace/local.py#L18); [README.md:60](https://github.com/OpenHands/software-agent-sdk/blob/84d4470acd85688b52c510532ffc83b234ebf453/README.md#L60) (verified)
  - *To reach the next level:* Least privilege requires the developer to pick a remote workspace and scrub the environment; L1 needs a narrower default.
- **B L0:** With no authorization layer, a hijacked agent can use every credential and file the OS user can reach (SSH keys, cloud CLIs, gh auth, the LLM key in the environment). — [openhands-sdk/openhands/sdk/utils/command.py:53](https://github.com/OpenHands/software-agent-sdk/blob/84d4470acd85688b52c510532ffc83b234ebf453/openhands-sdk/openhands/sdk/utils/command.py#L53); [README.md:60](https://github.com/OpenHands/software-agent-sdk/blob/84d4470acd85688b52c510532ffc83b234ebf453/README.md#L60) (verified)
  - *To reach the next level:* Blast radius is the user's whole account; L1 would need the agent's reachable credentials limited to a few systems.
- **Cap:** none

### C2 Approval gates — 0.42 (high)

OpenHands has a real per-action approval gate: with AlwaysConfirm or ConfirmRisky set, every batch of tool calls stops in a waiting state with the exact pending actions, and the caller can run or reject them. It is off by default: both the SDK conversation state and the agent server default to NeverConfirm, so the README setup runs shell commands and file edits with no human in the loop. When it is on, it covers every tool in the main loop including MCP tools, but it does not cover every sub-agent path. Little is reversible beyond the file editor's per-file undo.

- **S L3:** When enabled, the gate pauses every non-trivial action batch as WAITING_FOR_CONFIRMATION with the exact ActionEvents pending, supports explicit rejection, and ConfirmRisky can use deterministic pattern/policy analyzers as risk tiers. — [openhands-sdk/openhands/sdk/agent/agent.py:1130-1171](https://github.com/OpenHands/software-agent-sdk/blob/84d4470acd85688b52c510532ffc83b234ebf453/openhands-sdk/openhands/sdk/agent/agent.py#L1130-L1171); [openhands-sdk/openhands/sdk/security/confirmation_policy.py:27-32](https://github.com/OpenHands/software-agent-sdk/blob/84d4470acd85688b52c510532ffc83b234ebf453/openhands-sdk/openhands/sdk/security/confirmation_policy.py#L27-L32); [openhands-sdk/openhands/sdk/conversation/impl/local_conversation.py:2647](https://github.com/OpenHands/software-agent-sdk/blob/84d4470acd85688b52c510532ffc83b234ebf453/openhands-sdk/openhands/sdk/conversation/impl/local_conversation.py#L2647); [openhands-sdk/openhands/sdk/agent/agent.py:493](https://github.com/OpenHands/software-agent-sdk/blob/84d4470acd85688b52c510532ffc83b234ebf453/openhands-sdk/openhands/sdk/agent/agent.py#L493) (verified)
  - *To reach the next level:* Risk tiers commonly come from the model's own security_risk field (LLMSecurityAnalyzer) and there is no argument-level allow/deny policy in the gate itself; L4 needs that plus approval bound to an authenticated principal.
- **C L2:** Every tool call from the main loop, including MCP tools, goes through _requires_user_confirmation before execution, but it does not cover every sub-agent path. — [openhands-sdk/openhands/sdk/agent/response_dispatch.py:186](https://github.com/OpenHands/software-agent-sdk/blob/84d4470acd85688b52c510532ffc83b234ebf453/openhands-sdk/openhands/sdk/agent/response_dispatch.py#L186) (verified)
  - *To reach the next level:* L3 needs every path, including sub-agents, to reach the same human approval.
- **D L0:** Both the SDK ConversationState and the agent server's start-conversation request default to NeverConfirm. — [openhands-sdk/openhands/sdk/conversation/state.py:123](https://github.com/OpenHands/software-agent-sdk/blob/84d4470acd85688b52c510532ffc83b234ebf453/openhands-sdk/openhands/sdk/conversation/state.py#L123); [openhands-agent-server/openhands/agent_server/models.py:178](https://github.com/OpenHands/software-agent-sdk/blob/84d4470acd85688b52c510532ffc83b234ebf453/openhands-agent-server/openhands/agent_server/models.py#L178) (verified)
  - *To reach the next level:* Approval is opt-in; L1 needs it on by default.
- **B L1:** A wrongly executed action can run any shell command (delete files, push with ambient credentials, send data out); only file-editor edits have a per-file undo history. — [openhands-tools/openhands/tools/file_editor/editor.py:673](https://github.com/OpenHands/software-agent-sdk/blob/84d4470acd85688b52c510532ffc83b234ebf453/openhands-tools/openhands/tools/file_editor/editor.py#L673); [openhands-tools/openhands/tools/terminal/definition.py:89](https://github.com/OpenHands/software-agent-sdk/blob/84d4470acd85688b52c510532ffc83b234ebf453/openhands-tools/openhands/tools/terminal/definition.py#L89) (verified)
  - *To reach the next level:* No checkpoints or rollback for the workspace or external actions; L2 needs reversibility for the common case.
- **Cap:** G1 — The confirmation policy defaults to NeverConfirm in the SDK and the agent server, so the gate is opt-in.
- **Notes:** C2-POWERBYPASS was not applied separately: when enabled, the gate does cover the terminal; its absence in the default configuration is what G1 captures.

### C3 Tool & action scoping — 0.12 (high)

The default tools are general-purpose: the terminal tool takes an arbitrary shell string, and the file editor accepts any absolute path on the host with no workspace containment. The only argument check in the editor is that the path is absolute and exists (or does not, for create); an optional allowed_edits_files list exists but is not used by default. Tools are chosen by the developer, but the README and the default preset both include the shell and the editor. A misused tool can therefore reach the whole machine.

- **S L0:** The terminal tool executes a raw command string, and the file editor only checks that a path is absolute, not that it lies inside the workspace. — [openhands-tools/openhands/tools/terminal/definition.py:89](https://github.com/OpenHands/software-agent-sdk/blob/84d4470acd85688b52c510532ffc83b234ebf453/openhands-tools/openhands/tools/terminal/definition.py#L89); [openhands-tools/openhands/tools/file_editor/editor.py:635](https://github.com/OpenHands/software-agent-sdk/blob/84d4470acd85688b52c510532ffc83b234ebf453/openhands-tools/openhands/tools/file_editor/editor.py#L635) (verified)
  - *To reach the next level:* Raw shell passthrough and uncontained paths; L1 needs at least denylist filtering on arguments.
- **C L1:** Only the file editor validates (absolute path, existence, optional allowed_edits_files); the terminal has no argument validation. — [openhands-tools/openhands/tools/file_editor/impl.py:44](https://github.com/OpenHands/software-agent-sdk/blob/84d4470acd85688b52c510532ffc83b234ebf453/openhands-tools/openhands/tools/file_editor/impl.py#L44) (verified)
  - *To reach the next level:* Most tools do not validate inputs; L2 needs most built-in tools to validate.
- **D L1:** Agent.tools defaults to an empty list, but the README quick start and get_default_tools both enable the shell and the file editor (plus the browser in the preset). — [openhands-sdk/openhands/sdk/agent/base.py:128](https://github.com/OpenHands/software-agent-sdk/blob/84d4470acd85688b52c510532ffc83b234ebf453/openhands-sdk/openhands/sdk/agent/base.py#L128); [openhands-tools/openhands/tools/preset/default.py:56](https://github.com/OpenHands/software-agent-sdk/blob/84d4470acd85688b52c510532ffc83b234ebf453/openhands-tools/openhands/tools/preset/default.py#L56); [README.md:66](https://github.com/OpenHands/software-agent-sdk/blob/84d4470acd85688b52c510532ffc83b234ebf453/README.md#L66) (verified)
  - *To reach the next level:* Exec and write tools are in every documented default set; L2 needs selectable groups whose default excludes them.
- **B L0:** Any command against any host path or network destination is within designed use. — [openhands-tools/openhands/tools/terminal/definition.py:89](https://github.com/OpenHands/software-agent-sdk/blob/84d4470acd85688b52c510532ffc83b234ebf453/openhands-tools/openhands/tools/terminal/definition.py#L89); [openhands-sdk/openhands/sdk/workspace/local.py:18](https://github.com/OpenHands/software-agent-sdk/blob/84d4470acd85688b52c510532ffc83b234ebf453/openhands-sdk/openhands/sdk/workspace/local.py#L18) (verified)
  - *To reach the next level:* Tools reach the whole machine; L1 needs at least some limits on reach.
- **Cap:** none

### C4 Code-execution isolation — 0.42 (high)

In the default local setup there is no isolation: the terminal tool runs a tmux or bash process directly on the host as the user, and model-invoked skills can run shell snippets the same way. A Docker workspace is available on request, which runs the whole agent server and every tool inside a throwaway container with no host mounts by default, and fails if Docker cannot start. That container is a stock one, though: default capabilities, the image grants its user passwordless sudo, and the network is unrestricted. It is opt-in, so the criterion is capped.

- **default configuration** (default; raw 0.00 → 0.00)
  - **S L0:** LocalWorkspace operates on the host filesystem and the terminal backend spawns bash with subprocess.Popen (or a host tmux server) as the current user. — [openhands-sdk/openhands/sdk/workspace/local.py:18](https://github.com/OpenHands/software-agent-sdk/blob/84d4470acd85688b52c510532ffc83b234ebf453/openhands-sdk/openhands/sdk/workspace/local.py#L18); [openhands-tools/openhands/tools/terminal/terminal/subprocess_terminal.py:161](https://github.com/OpenHands/software-agent-sdk/blob/84d4470acd85688b52c510532ffc83b234ebf453/openhands-tools/openhands/tools/terminal/terminal/subprocess_terminal.py#L161) (verified)
    - *To reach the next level:* No isolation primitive in the default path; L1 needs at least filtering or a separate working directory boundary.
  - **C L0:** The main exec tool, skill inline commands, and hooks all run on the host. — [openhands-tools/openhands/tools/terminal/terminal/subprocess_terminal.py:161](https://github.com/OpenHands/software-agent-sdk/blob/84d4470acd85688b52c510532ffc83b234ebf453/openhands-tools/openhands/tools/terminal/terminal/subprocess_terminal.py#L161); [openhands-sdk/openhands/sdk/skills/execute.py:12-13](https://github.com/OpenHands/software-agent-sdk/blob/84d4470acd85688b52c510532ffc83b234ebf453/openhands-sdk/openhands/sdk/skills/execute.py#L12-L13) (verified)
    - *To reach the next level:* The main exec tool is not sandboxed; L1 needs it sandboxed.
  - **D L0:** Passing a path or LocalWorkspace (the README default) selects host execution. — [openhands-sdk/openhands/sdk/conversation/impl/local_conversation.py:380](https://github.com/OpenHands/software-agent-sdk/blob/84d4470acd85688b52c510532ffc83b234ebf453/openhands-sdk/openhands/sdk/conversation/impl/local_conversation.py#L380) (verified)
    - *To reach the next level:* Sandboxing is off by default; L1 needs it on.
  - **B L0:** Commands run with the user's full filesystem, network and environment, including any credentials in os.environ. — [openhands-sdk/openhands/sdk/utils/command.py:53](https://github.com/OpenHands/software-agent-sdk/blob/84d4470acd85688b52c510532ffc83b234ebf453/openhands-sdk/openhands/sdk/utils/command.py#L53); [openhands-sdk/openhands/sdk/workspace/local.py:18](https://github.com/OpenHands/software-agent-sdk/blob/84d4470acd85688b52c510532ffc83b234ebf453/openhands-sdk/openhands/sdk/workspace/local.py#L18) (verified)
    - *To reach the next level:* Host-equivalent reach; L1 needs at least the home directory and credentials kept out.
- **opt-in DockerWorkspace (agent server in a container)** (alt; raw 0.42, cap G1 → 0.42) ← counted
  - **S L2:** DockerWorkspace starts a stock container (--rm, default capabilities, no seccomp/cap-drop flags); the image's user has passwordless sudo, so it is effectively root inside. — [openhands-workspace/openhands/workspace/docker/workspace.py:241](https://github.com/OpenHands/software-agent-sdk/blob/84d4470acd85688b52c510532ffc83b234ebf453/openhands-workspace/openhands/workspace/docker/workspace.py#L241); [openhands-agent-server/openhands/agent_server/docker/Dockerfile:400](https://github.com/OpenHands/software-agent-sdk/blob/84d4470acd85688b52c510532ffc83b234ebf453/openhands-agent-server/openhands/agent_server/docker/Dockerfile#L400) (verified)
    - *To reach the next level:* No hardening (cap drop, no-new-privileges, non-sudo user, read-only root); L3 needs a hardened profile.
  - **C L3:** With a RemoteWorkspace the SDK returns a RemoteConversation, so the agent loop, tools, MCP servers and hooks all execute inside the container's agent server; a failed docker run raises instead of falling back to the host. — [openhands-sdk/openhands/sdk/conversation/conversation.py:155](https://github.com/OpenHands/software-agent-sdk/blob/84d4470acd85688b52c510532ffc83b234ebf453/openhands-sdk/openhands/sdk/conversation/conversation.py#L155); [openhands-workspace/openhands/workspace/docker/workspace.py:255](https://github.com/OpenHands/software-agent-sdk/blob/84d4470acd85688b52c510532ffc83b234ebf453/openhands-workspace/openhands/workspace/docker/workspace.py#L255) (verified)
    - *To reach the next level:* No operator-documented escape hatch is needed, but C cannot exceed S+1; L4 also needs a hardened primitive across spawned processes.
  - **D L0:** Docker isolation is chosen only when the developer constructs a DockerWorkspace; the default is LocalWorkspace. — [openhands-sdk/openhands/sdk/conversation/impl/local_conversation.py:380](https://github.com/OpenHands/software-agent-sdk/blob/84d4470acd85688b52c510532ffc83b234ebf453/openhands-sdk/openhands/sdk/conversation/impl/local_conversation.py#L380) (verified)
    - *To reach the next level:* Opt-in; L1 needs isolation on by default.
  - **B L1:** No host mounts by default and the container is removed on exit, but network egress is unrestricted unless a network is set, sudo is available inside, the LLM key lives in the server process, and SESSION_API_KEY is forwarded by default. — [openhands-workspace/openhands/workspace/docker/workspace.py:232](https://github.com/OpenHands/software-agent-sdk/blob/84d4470acd85688b52c510532ffc83b234ebf453/openhands-workspace/openhands/workspace/docker/workspace.py#L232); [openhands-workspace/openhands/workspace/docker/workspace.py:90](https://github.com/OpenHands/software-agent-sdk/blob/84d4470acd85688b52c510532ffc83b234ebf453/openhands-workspace/openhands/workspace/docker/workspace.py#L90) (verified)
    - *To reach the next level:* Full network egress with credentials reachable inside; L2 needs credentials kept out of the sandbox.
- **Cap:** G1 — Opt-in mechanism: off in the scored default configuration.
- **Notes:** An agent_sandbox (Kubernetes) workspace also exists; its template leaves gVisor commented out (openhands-workspace/openhands/workspace/agent_sandbox/deploy/sandboxtemplate.yaml:46). Any opt-in mechanism is capped at 0.50 by G1.

### C5 Untrusted input blast radius — 0.00 (high)

Nothing in code limits what a hijacked agent can do. Tool output from files, commands, MCP servers and the web enters the model's context as ordinary tool messages, with no provenance tracking; the only defence is a prompt notice that repository instruction files are untrusted. Optional security analyzers (pattern rules, an LLM judge, third-party guardrails) can escalate risky-looking actions to approval, but they are detection-based and off by default. In the default setup a prompt injection in a repository file can make the agent read secrets from the environment and send them anywhere, or delete and push, with no human involved.

- **S L0:** The only measure in the default flow is a prompt banner telling the model that repository-provided context is untrusted; there is no taint or provenance tracking (the search hits are shell-parse 'uncertainty' and profile 'provenance' metadata). — [openhands-sdk/openhands/sdk/context/prompts/sections/dynamic.py:61](https://github.com/OpenHands/software-agent-sdk/blob/84d4470acd85688b52c510532ffc83b234ebf453/openhands-sdk/openhands/sdk/context/prompts/sections/dynamic.py#L61); searched `rg -n -i 'taint|provenance'` in `openhands-sdk/openhands openhands-tools/openhands` → 15 hits (Hits are 'uncertainty' in the shell parser and launch/profile provenance metadata; none mark tool output as untrusted.) (verified)
  - *To reach the next level:* Prompt-only; L1 needs at least a detection layer on by default.
- **C L0:** Observations from every tool are converted to role=tool messages with no distinction by source. — [openhands-sdk/openhands/sdk/event/llm_convertible/observation.py:68](https://github.com/OpenHands/software-agent-sdk/blob/84d4470acd85688b52c510532ffc83b234ebf453/openhands-sdk/openhands/sdk/event/llm_convertible/observation.py#L68) (verified)
  - *To reach the next level:* Untrusted sources are not distinguished; L1 needs at least one source handled.
- **D L0:** The security analyzer defaults to None, so even the detection layer is off. — [openhands-sdk/openhands/sdk/conversation/state.py:126](https://github.com/OpenHands/software-agent-sdk/blob/84d4470acd85688b52c510532ffc83b234ebf453/openhands-sdk/openhands/sdk/conversation/state.py#L126) (verified)
  - *To reach the next level:* Off by default; L1 needs a control on by default.
- **B L0:** With NeverConfirm, a hijacked agent can read environment credentials and exfiltrate them through the shell's unrestricted network, and take irreversible actions, unattended. — [openhands-sdk/openhands/sdk/conversation/state.py:123](https://github.com/OpenHands/software-agent-sdk/blob/84d4470acd85688b52c510532ffc83b234ebf453/openhands-sdk/openhands/sdk/conversation/state.py#L123); [openhands-sdk/openhands/sdk/utils/command.py:53](https://github.com/OpenHands/software-agent-sdk/blob/84d4470acd85688b52c510532ffc83b234ebf453/openhands-sdk/openhands/sdk/utils/command.py#L53); [openhands-tools/openhands/tools/terminal/definition.py:89](https://github.com/OpenHands/software-agent-sdk/blob/84d4470acd85688b52c510532ffc83b234ebf453/openhands-tools/openhands/tools/terminal/definition.py#L89) (verified)
  - *To reach the next level:* Leak plus irreversible action without a human; L1 needs one of those two to require approval.
- **Cap:** C5-WORSTCASE — In the default configuration a hijacked agent can both exfiltrate secrets and take irreversible actions with no human involved.

### C6 Memory, context & configuration integrity — 0.20 (high)

The default setup keeps conversation history in memory only and auto-loads no instruction files, memory, or project skills, so poisoned context normally dies with the session. One workspace path is on by default, though: agent definition files in the project's .agents/agents and .openhands/agents folders are registered at startup, and they are not integrity-protected, which matters once a delegation tool is enabled. When the optional persistent memory or project skills are switched on, the agent writes memory freely and repository instruction files load silently, labelled untrusted only in the prompt.

- **S L0:** Project-level agent definition files are auto-registered and not integrity-protected; when enabled, MEMORY.md under the workspace is loaded into the system prompt with no validation. — [openhands-sdk/openhands/sdk/context/memory.py:23](https://github.com/OpenHands/software-agent-sdk/blob/84d4470acd85688b52c510532ffc83b234ebf453/openhands-sdk/openhands/sdk/context/memory.py#L23) (verified)
  - *To reach the next level:* L1 needs at least logged writes and controlled handling of repository-supplied configuration.
- **C L0:** Neither memory nor auto-loaded workspace files (agent definitions, AGENTS.md when project skills are on) pass any control. — [openhands-sdk/openhands/sdk/subagent/load.py:52-53](https://github.com/OpenHands/software-agent-sdk/blob/84d4470acd85688b52c510532ffc83b234ebf453/openhands-sdk/openhands/sdk/subagent/load.py#L52-L53) (verified)
  - *To reach the next level:* No path controlled; L1 needs one store controlled.
- **D L1:** Persistent memory and project skills are off by default; when enabled, the user-tier memory is shared across every project and the project tier sits in the agent-writable workspace. — [openhands-sdk/openhands/sdk/context/agent_context.py:127-128](https://github.com/OpenHands/software-agent-sdk/blob/84d4470acd85688b52c510532ffc83b234ebf453/openhands-sdk/openhands/sdk/context/agent_context.py#L127-L128); [openhands-sdk/openhands/sdk/context/agent_context.py:143-144](https://github.com/OpenHands/software-agent-sdk/blob/84d4470acd85688b52c510532ffc83b234ebf453/openhands-sdk/openhands/sdk/context/agent_context.py#L143-L144) (verified)
  - *To reach the next level:* No namespace enforcement once enabled; L2 needs per-session/project namespaces enforced in code.
- **B L3:** In the default configuration conversation state falls back to an in-memory store, so injected context is session-scoped. — [openhands-sdk/openhands/sdk/conversation/state.py:516-519](https://github.com/OpenHands/software-agent-sdk/blob/84d4470acd85688b52c510532ffc83b234ebf453/openhands-sdk/openhands/sdk/conversation/state.py#L516-L519) (verified)
  - *To reach the next level:* Agent-written .agents/agents files persist and are auto-registered next session; L4 needs persistence only after human review.
- **Cap:** none
- **Notes:** C6-REPOCONFIG was not applied: the auto-registered project agent definitions only change behaviour when a delegation tool (TaskToolSet/DelegateTool) is enabled, which neither the README nor get_default_agent does. Deployments that enable sub-agents should treat this cap as applying.

### C7 Third-party extensions — 0.30 (high)

Nothing third-party is loaded by default: MCP servers, plugins and public skills all start empty or disabled. When a developer adds them, sources are whatever they name: plugin refs are optional, public skills track the main branch of OpenHands' extensions repository, and MCP launch commands are taken as written, with no hash or signature checks. Plugin hooks run as shell commands on the host with nearly the full environment.

- **S L1:** Plugins take an optional ref; public skills default to the main branch; MCP servers run whatever command is configured. — [openhands-sdk/openhands/sdk/plugin/fetch.py:30](https://github.com/OpenHands/software-agent-sdk/blob/84d4470acd85688b52c510532ffc83b234ebf453/openhands-sdk/openhands/sdk/plugin/fetch.py#L30); [openhands-sdk/openhands/sdk/skills/skill.py:1146](https://github.com/OpenHands/software-agent-sdk/blob/84d4470acd85688b52c510532ffc83b234ebf453/openhands-sdk/openhands/sdk/skills/skill.py#L1146) (verified)
  - *To reach the next level:* No version pinning is required or verified; L2 needs pinned versions.
- **C L1:** Only plugins can be pinned (by ref, with the resolved commit SHA reported); MCP servers and public skills have no equivalent. — [openhands-sdk/openhands/sdk/plugin/fetch.py:80](https://github.com/OpenHands/software-agent-sdk/blob/84d4470acd85688b52c510532ffc83b234ebf453/openhands-sdk/openhands/sdk/plugin/fetch.py#L80) (verified)
  - *To reach the next level:* Most extension types lack any pinning; L2 needs most types covered.
- **D L2:** mcp_config defaults to an empty dict and public skills/plugins are off; extensions are added explicitly in code. — [openhands-sdk/openhands/sdk/agent/base.py:140](https://github.com/OpenHands/software-agent-sdk/blob/84d4470acd85688b52c510532ffc83b234ebf453/openhands-sdk/openhands/sdk/agent/base.py#L140); [openhands-sdk/openhands/sdk/context/agent_context.py:101-102](https://github.com/OpenHands/software-agent-sdk/blob/84d4470acd85688b52c510532ffc83b234ebf453/openhands-sdk/openhands/sdk/context/agent_context.py#L101-L102) (verified)
  - *To reach the next level:* Project agent definitions are not integrity-protected, and C/D cannot exceed S+1; L3 needs nothing third-party addable from the workspace.
- **B L1:** Plugin and workspace hooks run with shell=True in a separate process carrying sanitized_env(), i.e. the full host environment minus two session keys. — [openhands-sdk/openhands/sdk/hooks/executor.py:480](https://github.com/OpenHands/software-agent-sdk/blob/84d4470acd85688b52c510532ffc83b234ebf453/openhands-sdk/openhands/sdk/hooks/executor.py#L480); [openhands-sdk/openhands/sdk/hooks/executor.py:515](https://github.com/OpenHands/software-agent-sdk/blob/84d4470acd85688b52c510532ffc83b234ebf453/openhands-sdk/openhands/sdk/hooks/executor.py#L515) (verified)
  - *To reach the next level:* Extensions get the full environment; L2 needs a scrubbed environment per extension.
- **Cap:** none
- **Notes:** MCP stdio servers are launched through fastmcp/mcp; the environment those libraries pass to the child was not verified here.

### C8 Secrets & sensitive-data protection — 0.40 (high)

Credentials the developer registers as conversation secrets get good treatment: they are injected into commands only when referenced by name, masked out of command and MCP output before it reaches the model, and redacted when state is serialized. LLM keys are held as SecretStr and command logs are redacted. But anything already in the process environment, including the LLM key the README reads from LLM_API_KEY, is passed to every shell command and is not masked, so the model can simply print it. Telemetry and completion logging are off unless configured.

- **S L2:** api_key is SecretStr; SecretRegistry substitutes secrets by name at execution time and masks their values in tool output; command logging goes through redact_text_secrets. — [openhands-sdk/openhands/sdk/llm/llm.py:277](https://github.com/OpenHands/software-agent-sdk/blob/84d4470acd85688b52c510532ffc83b234ebf453/openhands-sdk/openhands/sdk/llm/llm.py#L277); [openhands-sdk/openhands/sdk/conversation/secret_registry.py:319](https://github.com/OpenHands/software-agent-sdk/blob/84d4470acd85688b52c510532ffc83b234ebf453/openhands-sdk/openhands/sdk/conversation/secret_registry.py#L319); [openhands-sdk/openhands/sdk/utils/command.py:99](https://github.com/OpenHands/software-agent-sdk/blob/84d4470acd85688b52c510532ffc83b234ebf453/openhands-sdk/openhands/sdk/utils/command.py#L99) (verified)
  - *To reach the next level:* No keychain or encryption at rest by default, and ambient environment secrets are not masked; L3 needs redaction before model-bound messages on all major paths.
- **C L2:** Logs, terminal and MCP output (for registered secrets) and serialized state are covered; subprocess environments are not. — [openhands-tools/openhands/tools/terminal/impl.py:371](https://github.com/OpenHands/software-agent-sdk/blob/84d4470acd85688b52c510532ffc83b234ebf453/openhands-tools/openhands/tools/terminal/impl.py#L371); [openhands-sdk/openhands/sdk/utils/command.py:53](https://github.com/OpenHands/software-agent-sdk/blob/84d4470acd85688b52c510532ffc83b234ebf453/openhands-sdk/openhands/sdk/utils/command.py#L53) (verified)
  - *To reach the next level:* Subprocess environments carry the host's secrets unmasked; L3 needs all listed paths covered.
- **D L2:** Laminar/OTel tracing activates only when its environment variables are set, completion logging defaults to False, and file logging is off by default. — [openhands-sdk/openhands/sdk/observability/laminar.py:28](https://github.com/OpenHands/software-agent-sdk/blob/84d4470acd85688b52c510532ffc83b234ebf453/openhands-sdk/openhands/sdk/observability/laminar.py#L28); [openhands-sdk/openhands/sdk/llm/llm.py:519-521](https://github.com/OpenHands/software-agent-sdk/blob/84d4470acd85688b52c510532ffc83b234ebf453/openhands-sdk/openhands/sdk/llm/llm.py#L519-L521); [openhands-sdk/openhands/sdk/logger/logger.py:35](https://github.com/OpenHands/software-agent-sdk/blob/84d4470acd85688b52c510532ffc83b234ebf453/openhands-sdk/openhands/sdk/logger/logger.py#L35) (verified)
  - *To reach the next level:* Tracing, when enabled, exports message content; L3 needs content-free opt-in telemetry with always-on redaction.
- **B L0:** The long-lived LLM key (read from LLM_API_KEY per the README) and any other exported credentials reach every subprocess the model starts. — [README.md:60](https://github.com/OpenHands/software-agent-sdk/blob/84d4470acd85688b52c510532ffc83b234ebf453/README.md#L60); [openhands-sdk/openhands/sdk/utils/command.py:53](https://github.com/OpenHands/software-agent-sdk/blob/84d4470acd85688b52c510532ffc83b234ebf453/openhands-sdk/openhands/sdk/utils/command.py#L53) (verified)
  - *To reach the next level:* Long-lived keys are reachable by the model; L1 needs them kept out of model-reachable subprocesses.
- **Cap:** none

### C9 Audit & traceability — 0.40 (high)

Every action, observation, rejection and message is a structured event with a source (agent, user, environment, hook) and timestamp, and with a persistence directory these are written to disk one file per event and can be resumed. By default, though, the SDK keeps events only in memory and prints them to the console, so nothing survives the process. The record does not name an approver or link sub-agent conversations, and it sits wherever the developer points it, with no tamper protection.

- **S L2:** Events carry a typed source and are appended per event to the file store when persistence is configured. — [openhands-sdk/openhands/sdk/event/types.py:5](https://github.com/OpenHands/software-agent-sdk/blob/84d4470acd85688b52c510532ffc83b234ebf453/openhands-sdk/openhands/sdk/event/types.py#L5); [openhands-sdk/openhands/sdk/conversation/event_store.py:232](https://github.com/OpenHands/software-agent-sdk/blob/84d4470acd85688b52c510532ffc83b234ebf453/openhands-sdk/openhands/sdk/conversation/event_store.py#L232) (verified)
  - *To reach the next level:* No approver identity, correlation IDs across sub-agents, or tamper evidence; L3 needs actor attribution.
- **C L2:** All main-loop tool calls (including MCP) and user rejections are events; sub-agents run in separate conversations. — [openhands-sdk/openhands/sdk/agent/response_dispatch.py:186](https://github.com/OpenHands/software-agent-sdk/blob/84d4470acd85688b52c510532ffc83b234ebf453/openhands-sdk/openhands/sdk/agent/response_dispatch.py#L186) (verified)
  - *To reach the next level:* Sub-agent activity is not tied into the parent record; L3 needs every tool path including sub-agents plus approvals.
- **D L0:** Without persistence_dir the state falls back to InMemoryFileStore and logs a warning. — [openhands-sdk/openhands/sdk/conversation/state.py:516-519](https://github.com/OpenHands/software-agent-sdk/blob/84d4470acd85688b52c510532ffc83b234ebf453/openhands-sdk/openhands/sdk/conversation/state.py#L516-L519); [openhands-sdk/openhands/sdk/conversation/impl/local_conversation.py:214](https://github.com/OpenHands/software-agent-sdk/blob/84d4470acd85688b52c510532ffc83b234ebf453/openhands-sdk/openhands/sdk/conversation/impl/local_conversation.py#L214) (verified)
  - *To reach the next level:* Durable recording is opt-in; L1 needs it on by default.
- **B L2:** When enabled, each event is written as it is appended, so records are flushed per action. — [openhands-sdk/openhands/sdk/conversation/event_store.py:232](https://github.com/OpenHands/software-agent-sdk/blob/84d4470acd85688b52c510532ffc83b234ebf453/openhands-sdk/openhands/sdk/conversation/event_store.py#L232) (verified)
  - *To reach the next level:* Not shown to block actions when a write fails; L3 needs durable per-action records with a replayable trajectory verified end to end.
- **Cap:** G1 — Durable event persistence requires the developer to pass persistence_dir; the default is an in-memory store.

### C10 Limits & kill switch — 0.33 (high)

Runs stop after 500 iterations by default and stuck-loop detection is on. A dollar budget exists but defaults to unlimited and is only exposed on LocalConversation, not the public Conversation factory. There is no wall-clock limit, and shell commands have only a 30-second no-output soft timeout that hands control back while the command keeps running. Pausing takes effect between steps; interrupt cancels an async run, but processes started in the terminal keep running, and sub-agents each get their own iteration and budget counters.

- **S L2:** An iteration cap and an optional per-run USD budget are enforced in the run loop; interrupt() cancels the async task and sets a cancellation token. — [openhands-sdk/openhands/sdk/conversation/impl/local_conversation.py:2022](https://github.com/OpenHands/software-agent-sdk/blob/84d4470acd85688b52c510532ffc83b234ebf453/openhands-sdk/openhands/sdk/conversation/impl/local_conversation.py#L2022); [openhands-sdk/openhands/sdk/conversation/impl/local_conversation.py:724](https://github.com/OpenHands/software-agent-sdk/blob/84d4470acd85688b52c510532ffc83b234ebf453/openhands-sdk/openhands/sdk/conversation/impl/local_conversation.py#L724); [openhands-sdk/openhands/sdk/conversation/impl/local_conversation.py:2772](https://github.com/OpenHands/software-agent-sdk/blob/84d4470acd85688b52c510532ffc83b234ebf453/openhands-sdk/openhands/sdk/conversation/impl/local_conversation.py#L2772) (verified)
  - *To reach the next level:* No wall-clock cap or rate limits, and halt does not kill spawned processes; L3 needs step, time and cost caps together.
- **C L1:** Limits apply per conversation; sub-agent conversations start fresh counters with the parent's per-run values, and terminal timeouts are soft. — [openhands-tools/openhands/tools/task/manager.py:266](https://github.com/OpenHands/software-agent-sdk/blob/84d4470acd85688b52c510532ffc83b234ebf453/openhands-tools/openhands/tools/task/manager.py#L266); [openhands-tools/openhands/tools/terminal/constants.py:31](https://github.com/OpenHands/software-agent-sdk/blob/84d4470acd85688b52c510532ffc83b234ebf453/openhands-tools/openhands/tools/terminal/constants.py#L31) (verified)
  - *To reach the next level:* Sub-agents don't share the parent's budget and tool timeouts don't stop processes; L2 needs tool timeouts enforced.
- **D L1:** Defaults are 500 iterations and no cost cap. — [openhands-sdk/openhands/sdk/conversation/impl/local_conversation.py:219](https://github.com/OpenHands/software-agent-sdk/blob/84d4470acd85688b52c510532ffc83b234ebf453/openhands-sdk/openhands/sdk/conversation/impl/local_conversation.py#L219); [openhands-sdk/openhands/sdk/conversation/impl/local_conversation.py:237](https://github.com/OpenHands/software-agent-sdk/blob/84d4470acd85688b52c510532ffc83b234ebf453/openhands-sdk/openhands/sdk/conversation/impl/local_conversation.py#L237) (verified)
  - *To reach the next level:* No default spend or time ceiling; L2 needs sensible defaults on all three.
- **B L1:** pause() takes effect only at the next iteration and does not stop commands already running in the host tmux session; no spend ceiling by default. — [openhands-sdk/openhands/sdk/conversation/impl/local_conversation.py:2721](https://github.com/OpenHands/software-agent-sdk/blob/84d4470acd85688b52c510532ffc83b234ebf453/openhands-sdk/openhands/sdk/conversation/impl/local_conversation.py#L2721) (verified)
  - *To reach the next level:* Stopping leaves terminal processes running; L2 needs moderate ceilings and in-flight calls to end with the loop.
- **Cap:** none

## Rule-of-Two check
[A] untrusted input: repository files, command output and fetched content read via the terminal and file editor, entering as role=tool messages (openhands-sdk/openhands/sdk/event/llm_convertible/observation.py:68) · [B] sensitive data/systems: the host user's files and full environment, including LLM_API_KEY (openhands-sdk/openhands/sdk/utils/command.py:53; README.md:60) · [C] state change / egress: arbitrary shell commands with unrestricted network on the host (openhands-tools/openhands/tools/terminal/terminal/subprocess_terminal.py:161) · Same default session? Yes

## Highest-impact improvements
1. Default the confirmation policy to AlwaysConfirm (or ConfirmRisky with the deterministic pattern/policy-rail analyzers) in both ConversationState and the agent server, with NeverConfirm as an explicitly named opt-out. — C2 D L0→L3, +0.150 before caps (Playbook 5, step 1)
2. Build terminal and hook environments from an allowlist (PATH, HOME, locale) plus explicitly registered secrets instead of copying os.environ. — C1 C L1→L2, +0.075 before caps (Playbook 4)
3. Route sub-agent confirmations to the parent's human approval path and harden handling of project-level agent files. — C2 C L2→L3, +0.075 before caps (Playbook 5)
4. Persist conversation events by default to a user-scope directory outside the workspace. — C9 D L0→L2, +0.100 before caps (Playbook 1, step 3)
5. Contain file-editor paths to the workspace with resolved-path checks and make a Docker workspace the documented default for autonomous runs. — C3 S L0→L2, +0.150 before caps (Playbook 3)

## Re-audit log
- No changes.

## Limitations
- Static source review of commit 84d4470 only; nothing was executed, installed, or probed.
- Framework scored by its README quick-start defaults; absent primitives score L0 even where a developer could add them.
- The OpenHands CLI, GUI (Agent Canvas) and Cloud live in other repositories and may set different defaults (e.g. confirmation mode, Docker runtime); they were not examined.
- The agent server (openhands-agent-server) was reviewed only for its defaults (NeverConfirm, telemetry consent); its HTTP authentication and multi-user handling were not scored.
- Environment handling for MCP stdio servers depends on the fastmcp/mcp libraries and was not verified.
- Defence-in-depth analyzers (pattern, policy rails, LLM, GraySwan, ToolShield) were read only at the level of how they plug into the confirmation gate; their detection quality was not assessed.
- No text aimed at AI reviewers was found in the repository.
