PRAXEN
agent behavior verifier
OpenHands Analysis Report
Completed May 29, 2026
7Findings
2Critical
3High
2Medium
RAISE maturity 1.75 / 5.0
Executive Summary
Agent Remit (as declared)
OpenHands is an autonomous software-engineering agent that accepts natural-language tasks and completes them end-to-end by writing and running code, browsing the web, and performing git and issue-tracker operations on connected hosted platforms. All agent-generated code is required to execute inside a sandboxed runtime that serves as the execution boundary, with all file I/O confined to the per-session workspace and untrusted input — task prompts, web pages, issue content, micro-agent and memory content — treated as prompt-injection-capable. The remit requires user confirmation for cross-repository writes, PR merges, destructive git operations, dependency commits, CI/CD edits, and new integration or MCP connections, and forbids running code on the host, leaking credentials into logs or model context, and cross-session state leakage.
Behavior Summary (as observed)

The dominant pattern is declared-but-unwired authentication: seven V1 routers — including secrets_router, settings_router, and git_router — carry the comment "The actual protection is provided by SetAuthCookieMiddleware" and signal protection to OpenAPI via get_dependencies(), but the OSS app.py never registers that middleware and get_dependencies() returns an empty list in the default OPENHANDS mode. The result is that the entire default-deployment V1 API — including the secrets endpoints that store and manage git provider tokens — is reachable unauthenticated, while LocalhostCORSMiddleware additionally falls fully open to any origin when no origins are configured.

Two structural choices compound this: the ProcessSandboxService runtime backend runs the agent-server as an unisolated host subprocess (the remit's host-isolation guarantee holds only on the Docker default), and skills/micro-agents enter agent context as raw content with no trust check despite the remit classifying micro-agent content as untrusted. Many strong remit clauses — sandbox path-escape rejection, tool-arg clamping, step caps, commit-content scanning — are enforced (if anywhere) in the extracted agentic core and are not verifiable from this control-plane snapshot.

Scope of Analysis
This snapshot is the V1 control plane only — a FastAPI application (openhands/app_server/app.py) exposing the /api/v1 router plus an embedded FastMCP server at /mcp; the agentic core (controller, runtime, llm, tool execution) was extracted to the separate Software Agent SDK and is out of scope. The app server registers only LocalhostCORSMiddleware, CacheControlMiddleware, and an in-memory RateLimitMiddleware — no authentication middleware — while routers signal protection to OpenAPI through get_dependencies(), which returns an empty list unless SESSION_API_KEY is set or app_mode is SAAS. Sandboxing is pluggable: DockerSandboxService (default, container-isolated) versus ProcessSandboxService (spawns the agent-server as an unisolated host subprocess); docker-compose.yml mounts the host Docker socket into the app container. Git provider tokens and custom secrets persist in a plaintext secrets.json via FileSecretsStore, and skills/micro-agents are fetched from the agent-server as raw content strings and loaded with no content-trust check.
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: 0 Gap: 0 Partial: 9 Vague Policy: 1 Enforcement Not Possible: 10 Total Rules: 20
Rule ID Section Rule (quoted) Status Finding
R-01 What OpenHands must always do "All agent-generated code MUST execute inside the sandboxed runtime and never directly on the host." Partial PRAX-2026-05-29-004
R-02 What OpenHands must always do "All file reads and writes MUST be confined to the per-session sandbox workspace, and any attempt to escape it — paths outside the workspace, escaping symlinks, or parent-directory traversal — MUST be rejected." Enforcement Not Possible
R-03 What OpenHands must always do "The user's task prompt, web-retrieved page content, issue descriptions, pull-request comments, repository file contents, and micro-agent or memory content MUST all be treated as untrusted input capable of carrying prompt-injection payloads." Partial PRAX-2026-05-29-005
R-04 What OpenHands must always do "Tool calls driven by LLM output MUST be validated at the boundary — argument shapes checked, numeric parameters clamped, and commands that reach outside the sandbox rejected." Enforcement Not Possible
R-05 What OpenHands must always do "Integration and session credentials MUST be verified on every request to an integration, and long-lived credentials MUST NOT be cached anywhere the model can reach." Partial PRAX-2026-05-29-001
R-06 What OpenHands must always do "Every action the agent takes — the tool invoked, its arguments, the outcome, and the time — MUST be recorded to a durable session record." Partial PRAX-2026-05-29-006
R-07 What OpenHands must always do "Per-session wall-clock and step-count caps MUST be enforced, and the agent MUST halt cleanly when a cap is exceeded." Enforcement Not Possible
R-08 What OpenHands must NEVER do "Agent-generated code MUST NOT run on the host operating system outside the sandboxed runtime." Partial PRAX-2026-05-29-004
R-09 What OpenHands must NEVER do "Files on the host outside the sandbox workspace MUST NOT be read, written, or referenced." Enforcement Not Possible
R-10 What OpenHands must NEVER do "Instructions embedded in untrusted content — web pages, issue descriptions and comments, source files in the workspace, or micro-agent and memory content — MUST NOT be followed when they attempt to exfiltrate credentials, API keys, or session tokens; escape the sandbox; redirect actions to a different repository, organization, or integration; commit or push changes without user confirmation; or open pull requests, close issues, or send messages beyond the current session's authorized scope." Partial PRAX-2026-05-29-005
R-11 What OpenHands must NEVER do "Secrets, credentials, tokens, or environment-file contents MUST NOT be committed to any git branch." Enforcement Not Possible
R-12 What OpenHands must NEVER do "Destructive git operations — force-push, branch deletion, history rewrite — MUST NOT be performed without user confirmation." Enforcement Not Possible
R-13 What OpenHands must NEVER do "API keys and session tokens MUST NOT leak into logs, error messages, or model context." Partial PRAX-2026-05-29-007
R-14 What OpenHands must NEVER do "One session's state, memory, or credentials MUST NOT leak into another session." Partial PRAX-2026-05-29-001
R-15 Human approval is required for "Writes to a repository or organization other than the one the task originated in MUST be confirmed by the user." Enforcement Not Possible
R-16 Human approval is required for "Merging a pull request MUST be confirmed by the user." Enforcement Not Possible
R-17 Human approval is required for "Adding a new MCP tool server at runtime MUST be confirmed by the user." Enforcement Not Possible
R-18 Authorized Counterparties "Any counterparty not listed here is unauthorized by default." Partial PRAX-2026-05-29-003
R-19 Out of Scope "OpenHands does not contact external services other than the LLM provider, the browser tool's fetches, the configured integrations, and configured MCP tool servers." Vague Policy
R-20 What OpenHands does NOT do "OpenHands MUST NOT run as an always-on background service that initiates tasks without a user request." Enforcement Not Possible
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.

CRITICAL PRAX-2026-05-29-001 OSS app server registers no auth middleware, leaving the entire V1 API — including the secrets endpoints that store git provider tokens — unauthenticated by default.
Policy Rule — R-05, R-14 (Worker Remit):
"Integration and session credentials MUST be verified on every request to an integration, and long-lived credentials MUST NOT be cached anywhere the model can reach. / One session's state, memory, or credentials MUST NOT leak into another session."
openhands/app_server/app.py:81 — only LocalhostCORSMiddleware, CacheControlMiddleware, and RateLimitMiddleware are added — no authentication middleware is registered on the OSS app openhands/app_server/utils/dependencies.py:23 — get_dependencies() returns [] unless _SESSION_API_KEY env var is set or app_mode==SAAS, so secrets/settings/git routers carry no auth dependency by default
Recommended Action
  • Register an authentication middleware in app.py for the OSS deployment (the SetAuthCookieMiddleware the router comments reference) so get_dependencies()-protected routes are enforced, or fail closed when no auth backend is configured.
  • Bind the OSS server to 127.0.0.1 by default and document that exposing it on a non-loopback interface requires SESSION_API_KEY or an external auth proxy.
CRITICAL PRAX-2026-05-29-002 Routers declare "protection provided by SetAuthCookieMiddleware" but the OSS app never registers that middleware, so the assumed control does not exist.
openhands/app_server/git/git_router.py:39 — comment "The actual protection is provided by SetAuthCookieMiddleware" — same comment in config_router, user_router, sandbox_router, sandbox_spec_router, app_conversation_router, event_router openhands/app_server/app.py:81 — grep for add_middleware shows only CORS/CacheControl/RateLimit; SetAuthCookieMiddleware is never registered in scope
Recommended Action
Either register SetAuthCookieMiddleware in the OSS app.py or remove the misleading comments and replace the OpenAPI-only get_dependencies() marker with a real fail-closed dependency.
HIGH PRAX-2026-05-29-003 LocalhostCORSMiddleware allows any origin with credentials when no CORS origins are configured, the default OSS state.
Policy Rule — R-18 (Worker Remit):
"Any counterparty not listed here is unauthorized by default."
openhands/app_server/middleware.py:43 — is_allowed_origin returns True for any origin when allow_origins and allow_origin_regex are unset ("Allow any origin when no specific origins are configured") openhands/app_server/config.py:103 — get_default_permitted_cors_origins() returns [] when no env var is set, so the fall-open branch is the default
Recommended Action
Default permitted_cors_origins to localhost-only and require explicit configuration; do not combine allow_credentials=True with a wildcard origin fallback.
HIGH PRAX-2026-05-29-004 The process-runtime backend spawns the agent-server as an unisolated host subprocess, so the remit's host-isolation guarantee holds only on the Docker default.
Policy Rule — R-01, R-08 (Worker Remit):
"All agent-generated code MUST execute inside the sandboxed runtime and never directly on the host. / Agent-generated code MUST NOT run on the host operating system outside the sandboxed runtime."
openhands/app_server/sandbox/process_sandbox_service.py:141 — subprocess.Popen(cmd, env=env, cwd=working_dir, ...) with env=os.environ.copy() — agent-server runs as a host process under a tempdir working dir, no container boundary openhands/app_server/config.py:341 — RUNTIME in ('local','process') selects ProcessSandboxServiceInjector, an explicit unisolated backend
Recommended Action
Document that the process runtime breaks the sandbox guarantee and is for trusted single-user dev only; warn loudly at startup when RUNTIME=process is combined with any network exposure.
HIGH PRAX-2026-05-29-005 Skills and micro-agents are loaded into agent context as raw content strings with no content-trust or injection check, despite the remit classifying them as untrusted.
Policy Rule — R-03, R-10 (Worker Remit):
"The user's task prompt, web-retrieved page content, issue descriptions, pull-request comments, repository file contents, and micro-agent or memory content MUST all be treated as untrusted input capable of carrying prompt-injection payloads. / Instructions embedded in untrusted content — web pages, issue descriptions and comments, source files in the workspace, or micro-agent and memory content — MUST NOT be followed when they attempt to exfiltrate credentials, API keys, or session tokens; escape the sandbox; redirect actions to a different repository, organization, or integration; commit or push changes without user confirmation; or open pull requests, close issues, or send messages beyond the current session's authorized scope."
openhands/app_server/app_conversation/skill_loader.py:361 — _convert_skill_info_to_skill builds Skill(content=skill_info.content, ...) directly from agent-server response with no content-trust validation openhands/app_server/app_conversation/skill_loader.py:326 — loop over data.get('skills', []) validates only the JSON shape (SkillInfo), never the trustworthiness of the content that enters context
Recommended Action
Treat skill/micro-agent content as untrusted: apply provenance checks and injection scanning before it enters the model context, and surface skill source/origin to the operator.
MEDIUM PRAX-2026-05-29-006 No structured, action-level control-plane audit log; the durable record captures conversation events but not auth decisions, secret access, or sandbox lifecycle.
Policy Rule — R-06 (Worker Remit):
"Every action the agent takes — the tool invoked, its arguments, the outcome, and the time — MUST be recorded to a durable session record."
openhands/app_server/event/filesystem_event_service.py:33 — _store_event writes per-conversation Event JSON to disk — durable, but scoped to agent conversation events, not control-plane actions openhands/app_server/secrets/secrets_router.py:249 — create_custom_secret stores a secret with no structured audit-log entry recording the actor or action
Recommended Action
Add a structured, append-only action log for control-plane operations (auth outcomes, secret CRUD, sandbox lifecycle) with actor identity and timestamp, separate from conversation events.
MEDIUM PRAX-2026-05-29-007 Git provider tokens and custom secrets persist in a plaintext secrets.json via FileSecretsStore rather than a vault.
Policy Rule — R-13 (Worker Remit):
"API keys and session tokens MUST NOT leak into logs, error messages, or model context."
openhands/app_server/secrets/file_secrets_store.py:33 — store() calls secrets.model_dump_json(context={'expose_secrets': True}) and writes it to secrets.json — plaintext token values at rest openhands/app_server/secrets/secrets_models.py:38 — Secrets model holds provider_tokens and custom_secrets, the git tokens exposed by the unauthenticated secrets endpoints
Recommended Action
Encrypt secrets at rest (the codebase already ships utils/encryption_key.py) or integrate an OS keychain / external vault; never serialize raw token values to a flat file by default.
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.

Default runtime is container-isolated

The default sandbox backend is DockerSandboxService, which runs the agent-server in a Docker container with a generated per-sandbox session API key; the unisolated process backend is opt-in via RUNTIME.

openhands/app_server/config.py:343

Per-request session-API-key check on the sandbox/webhook path

valid_sandbox requires an X-Session-API-Key header and resolves it against a running sandbox before any webhook action, with a per-sandbox key generated from os.urandom(32).

openhands/app_server/event_callback/webhook_router.py:230

Scoped, JWT-verified secret-retrieval endpoint

The /webhooks/secrets endpoint verifies a JWS access token scoped by user and provider type before returning a single provider secret, limiting blast radius if a token leaks.

openhands/app_server/event_callback/webhook_router.py:480

Dependencies pinned via committed lockfiles and pinned agent-server image

Both poetry.lock and uv.lock are committed and docker-compose.yml pins the agent-server image to tag 1.19.1-python, giving reproducible supply-chain builds.

docker-compose.yml:9
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
{persistence_dir}/{user_id}/v1_conversations/ FilesystemEventService (openhands/app_server/event/filesystem_event_service.py) per-event JSON files (SDK Event model) durable per-conversation event stream — agent actions and observations unknown Inferred
{sandbox_working_dir}/.openhands-agent-server.log ProcessSandboxService agent subprocess stdout/stderr plaintext process log agent-server process startup and runtime output for process-runtime sandboxes 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.

LLM03 Supply Chain
No findings
LLM04 Data and Model Poisoning
No findings
LLM05 Improper Output Handling
No findings
LLM06 Excessive Agency
No findings
LLM07 System Prompt Leakage
No findings
LLM08 Vector and Embedding Weaknesses
No findings
LLM09 Misinformation
No findings
LLM10 Unbounded Consumption
No findings
OWASP Agentic Top 10 (2026) Coverage

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

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.

1.75 / 5.0
Weighted Maturity Score · Ad hoc
Ad hoc. OpenHands' control plane has a coherent architecture with several operative controls — a default container-isolated sandbox, pinned dependency lockfiles, durable per-conversation event persistence, and a real session-API-key check on the sandbox/webhook path — but the framework's foremost runtime guarantee, request authentication, is not wired into the open-source application at all, leaving the whole V1 API (secrets included) open by default. Zero Trust is the weakest dimension because the authentication the routers assume exists is absent and CORS falls open when unconfigured; domain-limiting and supply-chain hygiene are genuinely Established, but the unauthenticated control plane and the silent host-process runtime downgrade dominate the weighted posture.
Limit Your Domain
3/ 5
Confidence: Medium  |  Weight: 15%  |  Weighted: 0.45
The agent's surface is scoped by an opt-in model — Docker sandbox by default, integrations and MCP servers added only by operator configuration — and tool inventory tracks the remit, but the topic/task domain itself is unrestricted by design (general SWE agent) and there is no code-level domain gate beyond the sandbox boundary.
Balance Your Knowledge Base
1/ 5
Confidence: Medium  |  Weight: 15%  |  Weighted: 0.15
Skills and micro-agents are fetched from the agent-server as raw content strings in skill_loader.py and converted directly into Skill objects with no content-trust or injection check, and the remit's untrusted-input validation for web/issue/memory content lives in the extracted agentic core, not verifiable here.
Implement Zero Trust
1/ 5
Confidence: High  |  Weight: 25%  |  Weighted: 0.25
The OSS app.py registers no authentication middleware and get_dependencies() returns an empty list in default OPENHANDS mode, so the entire V1 API (including the secrets endpoints holding git provider tokens) is unauthenticated by default, with LocalhostCORSMiddleware additionally allowing any origin when none is configured; the session-API-key check on the webhook/sandbox path is the only real interposition.
Manage Your Supply Chain
3/ 5
Confidence: Medium  |  Weight: 15%  |  Weighted: 0.45
Both poetry.lock and uv.lock are committed and the agent-server image is pinned to a specific tag in docker-compose.yml (1.19.1-python), giving reproducible builds; provenance review of the pulled agent-server image and the Tavily MCP proxy is not documented in this snapshot.
Build an AI Red Team
1/ 5
Confidence: Medium  |  Weight: 15%  |  Weighted: 0.15
A 64-file unit test suite exists under tests/unit/app_server (including webhook auth-path tests), but there is no adversarial, prompt-injection, or red-team testing of the skill content-trust path or the unauthenticated-API surface, and no evidence that security findings drove architecture.
Monitor Continuously
2/ 5
Confidence: Medium  |  Weight: 15%  |  Weighted: 0.30
A durable EventService persists per-conversation events (FilesystemEventService writes JSON files under persistence_dir; SQL variants also exist), but there is no structured, action-level audit log of control-plane operations themselves (auth decisions, secret reads/writes, sandbox lifecycle) — only Python logging calls and analytics events.

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.