OpenHands (Autonomous Software-Engineering Agent) Analysis Report
Completed July 11, 2026 · 28 artifacts examined
11Findings
1Critical
3High
6Medium
1Low
RAISE maturity 2.30 / 5.0
Executive Summary
Agent Remit (as declared)
OpenHands is an autonomous software-engineering agent that accepts natural-language tasks from an authenticated user and completes them end-to-end — writing code, executing it, browsing the web, and performing scoped Git / issue-tracker operations — with all agent-generated code and file I/O confined to a per-conversation sandboxed runtime. The remit covers two tightly-coupled components: the LLM-driven `CodeActAgent` from the pinned OpenHands Software Agent SDK (the primary RAISE subject) and the FastAPI app server that authenticates users, provisions sandboxes, brokers secrets, and stores events. Its authorized counterparties are the configured LLM provider, the user's explicitly connected Git/issue integrations, operator-configured MCP tool servers, and browser fetch targets — everything else is unauthorized by default. Standout obligations: secrets must be encrypted at rest and never leak into logs/trajectories/model context, high-impact actions (merge, force-push, cross-repo write) require human approval, and the agent must never execute on the host OS outside the sandbox.
Behavior Summary (as observed)
The dominant pattern is a capable, defensively-built server whose strongest safeguards (running-only session keys, per-user ownership checks, JWE token encryption, comprehensive log redaction, a durable event store, and a pinned CVE-tracked dependency set) are undercut by permissive OSS defaults on the exact controls the remit treats as mandatory. The default secrets store (`FileSecretsStore`) writes provider tokens and custom secrets to disk in plaintext despite a remit requirement to encrypt at rest; the default request path attaches no authentication (`get_dependencies()` returns an empty list unless `SESSION_API_KEY` is set); and CORS defaults to allowing any origin with credentials. These combine into a single exposure chain: an OSS instance reachable off-localhost serves unauthenticated, cross-origin reads of plaintext-at-rest credentials. Separately, the approval gates for high-impact Git actions depend on `confirmation_mode`, which resolves to `NeverConfirm()` when off (the headless default), so merges and force-pushes can execute without the human pause the remit requires.
Scope of Analysis
Python FastAPI application server under `openhands/app_server/`, pinned to the OpenHands SDK trio (`openhands-sdk`, `openhands-agent-server`, `openhands-tools`, all `==1.35.0`); the agentic `CodeActAgent` itself lives in those external pinned packages, not in this tree. Per-conversation isolation is provided by a Docker sandbox by default (`docker_sandbox_service.py`), with Remote and Process/local runtimes as opt-ins. User secrets and provider tokens persist through `FileSecretsStore`, which serializes them to a plaintext `secrets.json`; a `JwtService` (`services/jwt_service.py`) provides JWS/JWE but the OSS secrets store does not use it. Request auth is pluggable: the OSS default `DefaultUserAuth` returns no user and `get_dependencies()` attaches no auth dependency unless `SESSION_API_KEY` is set. The app also hosts its own FastMCP server (`mcp/mcp_router.py`) exposing PR-creation tools plus a Tavily proxy, and records every tool action to a durable per-conversation event store; the logging layer (`utils/logger.py`) applies aggressive secret redaction.
Remit Coverage

Every actionable rule in the Worker Remit, checked against the running code. Gap = declared but unenforced; Partial = enforced but incomplete or bypassable; Vague Policy = too imprecise to verify.

Verified: 11 Gap: 0 Partial: 7 Vague Policy: 0 Enforcement Not Possible: 2 Total Rules: 20
Rule ID Section Rule (quoted) Status Finding
R-01 Job Description "Execute agent-generated shell commands, Python / IPython, and other language runtimes inside the sandboxed runtime only, using the enabled agent tools (command, editor, Jupyter, browser, think, finish)." Verified
R-02 Job Description "Record every action — the tool invoked, its arguments, the outcome, and the timestamp — to a durable per-conversation event store." Verified
R-03 Job Description "Halt cleanly when a configured per-conversation cap (budget, iteration/step count, wall-clock, or max age) is reached." Partial PRAX-2026-07-11-006
R-04 Non-Goals (Out of Scope) "Executing agent-generated code, or reading/writing files, on the host operating system outside the sandboxed runtime." Verified
R-05 Approved Communication Channels "Adding a new server at runtime requires approval" Enforcement Not Possible
R-06 Data Boundaries "User-configured secrets held in the secrets store (must be encrypted at rest and returned masked by default)." Partial PRAX-2026-07-11-002
R-07 Forbidden Data Movement "API keys, tokens, or secret values written into logs, error messages, trajectories, or the model's context window." Verified
R-08 Forbidden Data Movement "Raw secret values returned to, or routed through, the SDK client rather than delivered SaaS→sandbox under the required dual authentication (Bearer token and an active-sandbox session key)." Verified
R-09 Job Description "Accept a natural-language software-engineering task from an authenticated user and carry it out end-to-end: plan, write code, execute it, debug, and iterate within a single conversation." Partial PRAX-2026-07-11-003
R-10 Action Boundaries "Merging a pull request." Partial PRAX-2026-07-11-004
R-11 Action Boundaries "Destructive Git operations: force-push, branch deletion, history rewrite." Partial PRAX-2026-07-11-004
R-12 Action Boundaries "Any action that would exceed the configured per-conversation step/iteration, budget, or time cap." Partial PRAX-2026-07-11-006
R-13 Action Boundaries "Running agent-generated code on the host OS outside the sandbox." Verified
R-14 Action Boundaries "Committing secrets, credentials, tokens, or .env contents to any Git branch." Enforcement Not Possible
R-15 Action Boundaries "Leaking API keys or session tokens into logs, error messages, trajectories, or model context." Verified
R-16 Behavioral Expectations "Each user is limited to a bounded number of concurrent conversations (default 3), each with its own isolated sandbox." Verified
R-17 Behavioral Expectations "Integration calls are re-authenticated per request against the requesting user's scoped credentials." Verified
R-18 Approval Requirements "Any high-impact action (exec / send / delete outside the sandbox) when the deployment runs with confirmation mode enabled — the agent must pause for user confirmation before executing." Partial PRAX-2026-07-11-004
R-19 Risk Sensitivities "Cross-conversation and cross-user isolation of sandboxes, events, and credentials." Verified
R-20 Escalation Rules "A secret value is detected in a location it should never occupy (log, error, trajectory, model context, or a Git diff)." Verified
Findings Register

Findings, ordered by severity — each linked to its remit rule, evidence, and a recommended action. Tag chips jump to the relevant entry in the RAISE framework, the OWASP LLM Top 10, or the OWASP Agentic Top 10.

LLM06 primary — the finding's main OWASP categoryASI10 secondary — a category it also touches

CRITICAL PRAX-2026-07-11-001 OSS defaults compose into an unauthenticated, cross-origin read of plaintext-at-rest provider tokens and secrets on any exposed instance.
Policy Rule — R-06, R-09 (Worker Remit):
"User-configured secrets held in the secrets store (must be encrypted at rest and returned masked by default). / Accept a natural-language software-engineering task from an authenticated user and carry it out end-to-end: plan, write code, execute it, debug, and iterate within a single conversation."
openhands/app_server/utils/dependencies.py:23 — get_dependencies() returns [] when SESSION_API_KEY is unset and app_mode is not SAAS — no auth dependency on the routers that mount it openhands/app_server/middleware.py:37 — is_allowed_origin returns True for any origin when permitted_cors_origins is empty (the default), with allow_credentials=True set at :29-35
Recommended Action
  • Ship a secure-by-default posture: require an auth credential (or bind strictly to loopback) unless the operator explicitly opts into open access, and refuse to serve wildcard-origin-with-credentials CORS when no origins are configured.
  • Encrypt secrets at rest in the default `FileSecretsStore` using the existing `JwtService.encrypt_value`/`decrypt_value` rather than persisting `expose_secrets=True` plaintext.
HIGH PRAX-2026-07-11-002 The default secrets store writes provider tokens and custom secrets to disk in plaintext, contrary to the encrypt-at-rest requirement.
Policy Rule — R-06 (Worker Remit):
"User-configured secrets held in the secrets store (must be encrypted at rest and returned masked by default)."
openhands/app_server/secrets/file_secrets_store.py:32 — store() calls secrets.model_dump_json(context={'expose_secrets': True}) then writes the plaintext JSON to the file store openhands/app_server/server_config/server_config.py:20 — secret_store_class defaults to '...file_secrets_store.FileSecretsStore' — the plaintext store is the OSS default
Recommended Action
Encrypt secret values before persistence (e.g. wrap each with `JwtService.encrypt_value`) and decrypt on load, or back the default store with an OS keyring/vault.
HIGH PRAX-2026-07-11-003 In the OSS default the app-server API attaches no authentication, so secrets/sandbox/settings endpoints are unauthenticated unless SESSION_API_KEY is set.
Policy Rule — R-09 (Worker Remit):
"Accept a natural-language software-engineering task from an authenticated user and carry it out end-to-end: plan, write code, execute it, debug, and iterate within a single conversation."
openhands/app_server/utils/dependencies.py:23 — get_dependencies() appends check_session_api_key only if _SESSION_API_KEY is truthy; otherwise no auth dependency is added for OSS mode openhands/app_server/user_auth/default_user_auth.py:32 — DefaultUserAuth.get_user_id always returns None (no multi-tenancy / no authenticated principal)
Recommended Action
Default to requiring an authentication credential (session key or token) for all state-changing and secret-bearing routes, and fail closed with a clear startup error when an externally-bound server has no auth configured.
HIGH PRAX-2026-07-11-004 Human-approval gates for merge, force-push, and cross-repo writes are inert unless confirmation mode is enabled, which is off by default headless.
Policy Rule — R-10, R-11, R-18 (Worker Remit):
"Merging a pull request. / Destructive Git operations: force-push, branch deletion, history rewrite. / Any high-impact action (exec / send / delete outside the sandbox) when the deployment runs with confirmation mode enabled — the agent must pause for user confirmation before executing."
openhands/app_server/app_conversation/app_conversation_service_base.py:681 — _select_confirmation_policy returns NeverConfirm() when confirmation_mode is false; ConfirmRisky/AlwaysConfirm only when enabled openhands/app_server/config.template.toml:53 — max_budget_per_task/max_iterations shown but confirmation_mode not enabled by default; headless resolves confirmation_mode=false
Recommended Action
Default confirmation mode (or an equivalent deterministic approval gate) to on for high-impact Git operations, and make the enabled gate independent of whether an LLM security analyzer is configured.
MEDIUM PRAX-2026-07-11-005 CORS defaults to allowing any origin with credentials when no permitted origins are configured.
openhands/app_server/middleware.py:44 — for missing configured origins the branch returns True for localhost AND for any other origin (development mode), with allow_credentials=True at :32 openhands/app_server/config.py:103 — get_default_permitted_cors_origins() returns [] by default, so the wildcard branch is the default path
Recommended Action
Do not combine wildcard-origin acceptance with `allow_credentials=True`; require an explicit origin allowlist for any non-loopback origin in production.
MEDIUM PRAX-2026-07-11-006 Per-task budget cap is disabled by default, so only the iteration cap bounds a runaway conversation's cost.
Policy Rule — R-03, R-12 (Worker Remit):
"Halt cleanly when a configured per-conversation cap (budget, iteration/step count, wall-clock, or max age) is reached. / Any action that would exceed the configured per-conversation step/iteration, budget, or time cap."
openhands/app_server/settings/settings_models.py:369 — max_budget_per_task: float | None = None — no per-task budget ceiling by default openhands/app_server/config.template.toml:53 — '#max_budget_per_task = 0.0' — commented, and 0.0 denotes no limit
Recommended Action
Ship a non-zero default `max_budget_per_task` (or refuse to start without one for hosted deployments) so cost caps halt runaway conversations without relying on operator configuration.
MEDIUM PRAX-2026-07-11-007 The global rate limiter is in-memory and keyed on client IP, so it is neither durable nor effective across multiple app-server processes.
openhands/app_server/middleware.py:78 — InMemoryRateLimiter keeps history in a process-local defaultdict keyed by request.client.host — no shared/distributed backing
Recommended Action
Back rate limiting with a shared store (e.g. Redis) and key it on the authenticated principal rather than client IP for multi-process/behind-proxy deployments.
MEDIUM PRAX-2026-07-11-008 The master encryption/JWT key is generated and stored in a plaintext file at rest and doubles as both the JWS signing key and JWE encryption key.
openhands/app_server/utils/encryption_key.py:82 — generated master key is dumped with expose_secrets=True and written to key_file (.keys) in cleartext openhands/app_server/services/jwt_service.py:184 — same key.get_secret_value() is used to sign JWS (HS256) and to derive the A256GCM JWE key
Recommended Action
Source the master key from an OS keyring / KMS / vault rather than a cleartext file, and consider separating the signing key from the encryption key.
MEDIUM PRAX-2026-07-11-009 No adversarial or prompt-injection testing evidence exists in the scanned tree for an agent that ingests untrusted web and repository content.
tests/unit — unit tests present but no adversarial/prompt-injection test artifacts or red-team documentation found in the scanned app-server tree
Recommended Action
Add an indirect-prompt-injection test suite exercising untrusted web/issue/microagent content and record how findings feed back into controls (e.g. the security analyzer default).
MEDIUM PRAX-2026-07-11-010 Structured JSON logging is disabled by default, so the app-server application log is unstructured for automated detection.
openhands/app_server/utils/logger.py:30 — LOG_JSON defaults to False; JSON formatter only attached when LOG_JSON is truthy
Recommended Action
Default `LOG_JSON` to on (or document it as required) for hosted deployments so app-server logs are machine-parseable alongside the event store.
LOW PRAX-2026-07-11-011 The Tavily MCP proxy embeds the configured API key in the connection URL query string.
openhands/app_server/mcp/mcp_router.py:66 — StreamableHttpTransport(url=f'https://mcp.tavily.com/mcp/?tavilyApiKey={tavily_api_key}') — API key in URL query string
Recommended Action
Pass the Tavily credential via an Authorization header or the transport's auth mechanism instead of the URL query string.
What's Working Well

Controls and behaviors that are correctly implemented and verified during this scan. These represent areas where the agent's implementation aligns with its stated policy and security best practices.

Layered secret redaction across all log surfaces

`SensitiveDataFilter` scrubs env-derived secrets and known key patterns, `RedactURLParamsFilter` strips query-string secrets (e.g. `session_api_key`) from uvicorn access logs, LiteLLM loggers are disabled, and `DEBUG_LLM` requires an interactive confirmation — directly implementing the remit's never-leak-secrets-to-logs rule.

openhands/app_server/utils/logger.py:248-300

Session keys valid only for RUNNING sandboxes with ownership verification

`validate_session_key` rejects keys for paused/stopped/deleted sandboxes and `validate_session_key_ownership` confirms the sandbox belongs to the caller, limiting the blast radius of a leaked session key.

openhands/app_server/sandbox/session_auth.py:37-139

Exactly-pinned, CVE-tracked dependency set with dual lockfiles

The SDK trio is pinned `==1.35.0` and most dependencies carry exact pins with CVE-remediation comments, backed by committed `poetry.lock` and `uv.lock` and a `uv exclude-newer` guard.

pyproject.toml:161-259

Secret-name validation blocklist for reserved container/LLM env

`validate_secret_name` blocks user secrets that would override agent-server container config, `LLM_*` controls, or webhook/CORS callback env vars, preventing a class of sandbox-config and callback-redirection abuse.

openhands/app_server/constants.py:44-129

Durable per-conversation event store recording every action

The webhook `on_event` handler persists each event to the configured event store (filesystem/S3/GCS), giving an action-level, timestamped, durable record sufficient to reconstruct a conversation.

openhands/app_server/event_callback/webhook_router.py:468-489

JWE token encryption pinned to a single safe algorithm

`JwtService` restricts its JWE registry to `dir`+`A256GCM` only, closing cryptographic-agility downgrade attacks on encrypted tokens.

openhands/app_server/services/jwt_service.py:27-27

Secrets serialized masked by default

The `Secrets` model masks provider tokens and custom secret values unless an explicit `expose_secrets` serialization context is supplied, so ordinary API reads (e.g. `/secrets/search`) never return raw values.

openhands/app_server/secrets/secrets_models.py:58-107
Discovered Log Files

Log files found in the agent's workspace during this scan. Reviewing these files provides runtime evidence to complement the static analysis above.

Path Source Content Type Purpose Last Modified Status
openhands/app_server/<file_store>/events/<conversation_id>/*.json openhands/app_server/event/filesystem_event_service.py structured JSON event records durable per-conversation action/decision log (every tool invocation, args, outcome, timestamp) unknown Inferred
logs/openhands.log openhands/app_server/utils/logger.py (TimedRotatingFileHandler) plaintext or JSON lines (LOG_JSON) app-server application log with layered secret redaction unknown Inferred
logs/llm/<session>/{prompt,response}_NNN.log openhands/app_server/utils/logger.py (LlmFileHandler) plaintext LLM prompt/response transcripts LLM request/response debugging (DEBUG / LOG_TO_FILE only) unknown Inferred
OWASP LLM Top 10 (2025) Coverage

Each card represents one category and shows the top 3 findings. All items in the Findings section.

OWASP Agentic Top 10 (2026) Coverage

Each card represents one category and shows the top 3 findings. All items in the Findings section.

ASI01 Agent Goal Hijack
No findings
ASI04 Agentic Supply Chain Vulnerabilities
No findings
ASI05 Unexpected Code Execution (RCE)
No findings
ASI06 Memory and Context Poisoning
No findings
ASI07 Insecure Inter-Agent Communication
No findings
ASI08 Cascading Failures
No findings
ASI09 Human-Agent Trust Exploitation
No findings
ASI10 Rogue Agents
No findings
RAISE Maturity Posture

Overall maturity assessment across the six categories of the RAISE framework. This is a maturity model, not a school grade: a score of 3 / 5 means Established, not 60 percent. Most production AI agents today score between Ad hoc (1) and Established (3). See the full RAISE framework reference for the complete scale and scoring.

2.30 / 5.0
Weighted Maturity Score · Partial
Partial posture overall: OpenHands has real, operative controls across most of the framework but leaves several remit-mandated protections off by default. Supply-chain management and continuous monitoring are its strongest dimensions — an exactly-pinned, CVE-annotated dependency set with dual lockfiles, and a durable per-conversation event store backed by strong log redaction. Zero Trust, weighted double, is where the gaps concentrate: no request authentication in the OSS default, plaintext secrets at rest, default-open CORS with credentials, and approval gates that are inert unless confirmation mode is switched on. Domain limitation and knowledge-base balance sit in the middle — the sandbox and per-tool enable flags scope the agent, but untrusted web/repo content reaches the model with injection defenses (the LLM security analyzer) disabled by default, and there is no adversarial-testing evidence in the scanned tree.
Limit Your Domain
3/ 5
Confidence: Medium  |  Weight: 15%  |  Weighted: 0.45
The agent is scoped to software-engineering work inside a per-conversation sandbox with individually enable-flagged tools (`enable_cmd`, `enable_editor`, `enable_jupyter`, `enable_browsing`) in `config.template.toml`, but it is a deliberately general-purpose code agent and the domain enforcement beyond the sandbox boundary lives in the pinned SDK rather than an app-server code gate.
Balance Your Knowledge Base
2/ 5
Confidence: Medium  |  Weight: 15%  |  Weighted: 0.30
External untrusted content (web pages via the browser tool, repository files, issue/PR bodies, microagents) flows into the model context by design, and the injection mitigation — the `LLMSecurityAnalyzer` selected in `app_conversation_service_base.py` — is only wired in when `confirmation_mode`/`security_analyzer` are configured, defaulting to none.
Implement Zero Trust
2/ 5
Confidence: Medium  |  Weight: 25%  |  Weighted: 0.50
Genuine controls exist — running-only session-key validation with ownership checks (`sandbox/session_auth.py`), JWE with a restricted `dir`+`A256GCM` registry, and secret-name blocklists (`constants.py`) — but the OSS defaults leave the API unauthenticated (`utils/dependencies.py` `get_dependencies()` empty unless `SESSION_API_KEY` is set), secrets in plaintext at rest (`file_secrets_store.py`), CORS open to any origin with credentials (`middleware.py`), and high-impact approval gates inert under `NeverConfirm()`.
Manage Your Supply Chain
3/ 5
Confidence: High  |  Weight: 15%  |  Weighted: 0.45
The security-critical SDK trio is exactly pinned (`openhands-sdk`/`openhands-agent-server`/`openhands-tools` `==1.35.0`) and most dependencies carry exact pins with explicit CVE-remediation comments (`authlib`, `orjson`, `aiohttp`, `protobuf`) plus committed `poetry.lock` and `uv.lock`; a handful of floor/caret ranges (`fastmcp>=3.2,<4`, `kubernetes`, `pygithub`, `asyncpg`) remain but are constrained by the lockfiles.
Build an AI Red Team
1/ 5
Confidence: Low  |  Weight: 15%  |  Weighted: 0.15
The scanned app-server tree contains a conventional pytest unit-test suite but no adversarial or prompt-injection test artifacts, red-team reports, or evidence that adversarial findings drove architectural change for an agent that ingests untrusted web and repository content.
Monitor Continuously
3/ 5
Confidence: High  |  Weight: 15%  |  Weighted: 0.45
Every tool action is persisted to a durable per-conversation event store (`event/filesystem_event_service.py` via the `/webhooks/events` handler saving each event), and the logging layer applies layered secret redaction; structured JSON logging exists but is off by default (`LOG_JSON=false`), so the primary durable action record is the event store rather than the app log.

Maturity Scoring Rubric

Every score above is based on this scale. A score is a snapshot of observable posture — not a verdict on the people or team behind the system.

Score Label Meaning
5 Exemplary Best-in-class; automated, continuously tested, reference quality. Rarely achieved in shipping systems.
4 Strong Comprehensive controls, active management, minor gaps. Production-ready.
3 Established Documented controls consistently applied; known gaps accepted. A respectable baseline.
2 Partial Some controls exist but coverage is incomplete; key gaps remain.
1 Ad hoc Informal or inconsistent measures; relies on individual judgment.
0 Absent No evidence this category is addressed at all.
Weighting: the weighted overall above is the sum of each category's score × weight (the per-category weights are shown on each card). Zero Trust carries double weight by design; see the RAISE framework reference for the rationale.