OpenHands
OpenHands connector wires DefenseClaw into global ~/.openhands/hooks.json command hooks by default, with MCP discovery through ~/.openhands/mcp.json and optional workspace-local skills.
The OpenHands connector wires DefenseClaw into OpenHands lifecycle hooks. DefenseClaw installs the hook globally at ~/.openhands/hooks.json by default so local OpenHands sessions across repositories report into the same DefenseClaw gateway, while OpenHands still talks directly to its configured upstream model provider.
Platform support
| Platform | Status | Notes |
|---|---|---|
| macOS and Linux | Supported | DefenseClaw installs the POSIX hook runtime and supports the OpenHands CLI >=1.12.0 hook contract. macOS can additionally use the protected launch-command trace-only native OTLP path; Linux remains hook-only. |
| Native Windows x64 | Unsupported | OpenHands CLI requires WSL, and DefenseClaw does not implement a WSL connector path. |
Setup
defenseclaw setup openhands # observe (default) - record only
defenseclaw setup openhands --mode action # block on policy hits via hook deny
defenseclaw setup openhands --workspace /path/to/repoNative Windows setup is unsupported
DefenseClaw has no native Windows or WSL connector path for OpenHands. There is no supported Git Bash, PowerShell, WSL, Docker, or mixed-path workaround; run this connector on macOS or Linux instead.
By default this is global/user-scoped: DefenseClaw writes native OpenHands hook entries into ~/.openhands/hooks.json and installs the shared hook script at ~/.defenseclaw/hooks/openhands-hook.sh. Pass --workspace /path/to/repo only when you intentionally want repo-local .openhands/hooks.json wiring for that repository. There is no proxy-enforcement path for OpenHands — blocking happens hook-side with OpenHands' documented exit code 2 deny response.
The OpenHands connector is hook-only. defenseclaw setup openhands defaults to observability-only mode=observe, and defenseclaw setup openhands --mode action enables hook-native blocking. OpenHands still talks directly to its configured upstream model provider in both modes; DefenseClaw never inserts an LLM proxy.
What setup openhands actually does
The wrapper accepts the same hook-alias flags as the other hook connectors. The underlying guardrail config falls back to the values DefenseClaw ships with — schema-defined in internal/config/config.go and documented on the Defaults page.
| Flag | Default | What it does |
|---|---|---|
--yes / -y | off | Skip the confirmation prompt. |
--mode observe|action | observe | observe records only; action returns OpenHands' documented deny response on policy hits. |
--restart / --no-restart | --restart | Bounce defenseclaw-gateway after applying changes so the new hooks wire in. |
--with-local-stack / --no-local-stack | --no-local-stack | Also run setup local-observability up; follow the command's printed gateway-restart step after it writes the export destination. |
--workspace / --workspace-dir | unset | Opt into repo-local .openhands/hooks.json; unset means global ~/.openhands/hooks.json. |
setup openhands is the dedicated OpenHands alias. It uses the shared
guardrail setup backend, defaults to observe mode, and can join an existing
hook-connector roster when you choose Add. claw.workspace_dir is cleared
for global setup and set only when --workspace is supplied. See the
quick-alias reference for common options and
full guardrail setup for advanced scanner and judge
configuration.
Version contract
DefenseClaw validates the installed OpenHands CLI version during setup when the trusted openhands --version probe succeeds. The supported POSIX contract starts at OpenHands CLI 1.12.0, the first version that loads user-global ~/.openhands/hooks.json. It is source-reviewed against OpenHands CLI 1.16.0 with no maximum version currently declared.
Global OpenHands setup observes local OpenHands sessions that load ~/.openhands/hooks.json. OpenHands 1.16.0 loads a repository-local .openhands/hooks.json instead of the global file, not next to it: in a project that has its own file, none of DefenseClaw's global hooks run, so tool calls there are not inspected or logged. Use --workspace when you want DefenseClaw to manage a repo-local file explicitly. In an enterprise deployment the guardian writes only the global file, and the foreign-hook guard does not cover OpenHands; see the enterprise residual risks.
Hook schema
DefenseClaw writes the native OpenHands shape: top-level snake_case event keys, each containing matcher groups and command hooks. It does not wrap the config in a Claude Code-style top-level hooks object.
{
"pre_tool_use": [
{
"matcher": "*",
"hooks": [
{
"type": "command",
"command": "/home/<you>/.defenseclaw/hooks/openhands-hook.sh",
"timeout": 60
}
]
}
]
}The generated value is the absolute, shell-quoted hook-script path for the
current DefenseClaw data directory; ~ is not written literally.
OpenHands can block pre_tool_use, user_prompt_submit, and stop. Non-blocking events still feed telemetry and audit. When DefenseClaw blocks, the hook script returns JSON shaped like OpenHands expects:
{ "decision": "deny", "reason": "policy denied" }OpenHands documents additionalContext for context injection. DefenseClaw uses that field for downgraded alerts or confirm verdicts because OpenHands does not expose a native ask/approval surface in the documented hook contract.
Local surfaces
OpenHands MCP servers are managed in ~/.openhands/mcp.json, so MCP discovery is global by default. Current OpenHands skills use .agents/skills; global setup resolves that as ~/.agents/skills, and workspace setup resolves it under the pinned repository. DefenseClaw also discovers deprecated .openhands/skills and .openhands/microagents paths, installed skills in ~/.openhands/skills/installed, and the public OpenHands extensions cache at ~/.openhands/cache/skills/public-skills/skills. That cache is where OpenHands' own "Loaded: N skills" count comes from after the SDK syncs public skills. OpenHands has no plugin install surface in the documented local contract, so DefenseClaw treats plugins as unsupported and hides the Plugins TUI panel while claw.mode=openhands.
claw.workspace_dir is optional. Empty means global user scope. When set via --workspace, workspace-local skills/rules discovery uses that repository until you clear it with a global setup run or point it at another workspace.
The resolved paths are captured in ~/.defenseclaw/hook_contract_lock.json under locations, so defenseclaw doctor can report both the hook contract and the concrete hook/MCP/skill locations it is using.
Hook capabilities
Block events
- pre_tool_use
- user_prompt_submit
- stop
Native ask events
None — confirm verdicts are downgraded with the raw action preserved.
OpenHands can block supported hook events but has no native human-approval surface. Confirm verdicts become an additionalContext alert with raw_action: "confirm" preserved; the TUI and audit log are review-only. In the standalone enterprise profile, a confirm verdict on PreToolUse is blocked instead, and the message says the organization's rule needs a confirmation that OpenHands cannot ask for.
OpenShell sandbox
DefenseClaw can build an OpenHands OpenShell overlay image. The files below exist only in the image; nothing on your host changes.
-
Hooks in the user tier. OpenHands CLI has no system or managed hook location. It loads the first of
<workdir>/.openhands/hooks.jsonand~/.openhands/hooks.jsonthat exists, once per conversation. The image seeds the reviewed~/.openhands/hooks.jsonand keeps a root-owned copy under/usr/local/lib/defenseclaw/openhands/. The launcher restores the user file from that copy before every start, so an edit made during a session is undone at the next launch. If~/.openhands/hooks.jsonis anything but a regular file (a directory, for example), the launcher refuses to start OpenHands and names the path; remove it and start again. -
Project hook files are refused. A working directory with its own
.openhands/hooks.jsonwould replace DefenseClaw's hooks, so the launcher refuses to start there with a message that names the file. Rename the file to run OpenHands in the sandbox. -
Fail closed. The sandbox hook exits 2, the status OpenHands enforces, on every failure: missing token, ingress down, or a bad reply.
-
Launch environment. The launcher drops every
PYTHON*variable, so aPYTHONPATHexported from a shell start-up file cannot load code into OpenHands. -
Session end. OpenHands 1.16 closes the conversation after its TUI has stopped and then hands the
SessionEndhook results to that TUI, which printedException ignored in atexit callbackand aRuntimeError: App is not runningtraceback above DefenseClaw's session summary at every exit. A root-owned module in the image runs theSessionEndhooks as before and drops only that display event, so the session ends cleanly and DefenseClaw still gets the hook. The module also ignores theAuthlibDeprecationWarning(authlib.jose module is deprecated) an OpenHands dependency printed at every start. -
Blocks on screen. OpenHands shows a hook result as a collapsed line cut at 70 characters, which left
Status: BLOCKED - Blocked by DefenseCla.... The same module starts a DefenseClaw block's rendering with the block, so the collapsed line readsBLOCKED by DefenseClaw rule <ID>: <title>...; Ctrl+O still expands it to the full reason and OpenHands' own details. The reason the model gets is unchanged. -
Screen updates. OpenHands 1.16 sometimes does not scroll to a new turn: the token counter moves but the view stays on the previous turn, and PageDown does nothing. Pressing Ctrl+O twice redraws it. This is OpenHands' own TUI, not DefenseClaw.
-
Known gaps. The agent can edit
~/.openhands/hooks.json, or start a second OpenHands with anotherHOME, from a tool call. DefenseClaw sees that tool call first, and hook-silence detection is the backstop. Command rules judge the terminal tool's commands. Text the terminal tool sends to a running process (is_input) is judged as a shell command too. A command that a script runs, when the agent writes the script to a file and runs it, is judged by the command that starts the script. -
Install pin. The image installs OpenHands CLI 1.16.0 from PyPI as a root-owned
uv toolon a private CPython 3.12.13. The build refuses a version below the reviewed hook contract (>=1.12.0). -
Model access. Every DefenseClaw provider profile runs OpenHands with
--override-with-envs. The launcher copies the profile's credential placeholder intoLLM_API_KEY; you choose the model withLLM_MODEL, a LiteLLM name.defenseclaw sandbox run openhandspicks a profile from the first set ofOPENAI_API_KEY,ANTHROPIC_API_KEYandAWS_BEARER_TOKEN_BEDROCK(--llmpicks one):defenseclaw-openai:openai/<model>defenseclaw-anthropic:anthropic/<model>- the regional
defenseclaw-bedrock-mantle-openai-<region>:openai/<Mantle model id>, withLLM_BASE_URLset to the Mantle route
-
Launch flags. Skip-permissions mode adds
--always-approve. A run with--safedrops--always-approve,--yoloand--llm-approve, and every prefix OpenHands accepts for them. A headless run isopenhands --headless --exit-without-confirmation -t <prompt>; pass-- --headless -t "TEXT"tosandbox runfor a detached run. OpenHands' headless mode approves every action itself, so--safekeeps its prompts only in interactive runs. -
Other traffic. Apart from the model, a mock-model run reached only
pypi.org(the version check),github.comandraw.githubusercontent.com(the public skills download). That traffic goes through the DefenseClaw egress proxy, which logs it and applies the blocklist. OpenShell also logged about 20 refused attempts to reach the sandbox's own hostname; OpenHands worked regardless. -
Status. Verified end to end. A run through the DefenseClaw daemon in an OpenShell sandbox, on a mock model behind a credential binding, showed:
- hooks reaching the sandbox ingress, with OpenShell substituting the model key;
- a command that a DefenseClaw rule blocks refused by the hook, with the rule's reason passed back to the model;
- egress through the proxy, including a blocklisted host that a sandbox-scoped unblock then let through.
The image also passes the hook-fire probe. In that probe a replaced user
hooks.jsonis restored, a plantedsitecustomizemodule in the launch environment never runs, and the launcher refuses to start when the hooks file is a directory. The curated provider profiles have not carried a real model inside a sandbox; see the harness's verification note for what was run.
Enterprise deployments
In an enterprise deployment on Linux and macOS the
guardian writes DefenseClaw's hooks into each enrolled user's
~/.openhands/hooks.json and repairs them when the user changes them.
OpenHands has no machine-wide hook location, so the hooks live in a file the
user owns, and OpenHands coverage is advisory against a user who sets out to
avoid it:
- Project hook files replace DefenseClaw's hooks. OpenHands looks for
.openhands/hooks.jsonin the working directory first, then for~/.openhands/hooks.json, and uses only the first file it finds. In a project that has its own file, even one with a single unrelated hook, none of DefenseClaw's hooks run (OpenHands reportsLoaded: ... 1 hookinstead of 6), and no DefenseClaw message is shown. A repository a developer clones can carry the file. The guardian does not restore or remove project files, the foreign-hook guard does not cover OpenHands, and status and verify do not report it. - Older OpenHands releases. OpenHands before 1.12.0 does not load
~/.openhands/hooks.json. A user can start one without installing it, for example withuvx --from openhands==1.11.0 openhands, and its tool calls then run with no audit record. Nothing refuses it, and becauseuvxruns it from its cache, it is not reported. - Another home. OpenHands reads the hook file under
HOME, so a session started withHOMEpointing elsewhere loads no DefenseClaw hooks. The guardian never repairs that session, and status and verify keep reporting the user's OpenHands ready.OPENHANDS_PERSISTENCE_DIRandDEFENSECLAW_*environment overrides do not remove the hooks. - A killed or stalled hook lets the call run.
openhands-hook.shruns as the user. OpenHands treats a killed hook (Exit Code: -9) and can treat a hook that stalls until its 60-second timeout as non-blocking, so the call runs with no DefenseClaw decision; OpenHands' own confirmation prompt, which the user answers, is the only check left.
What reduces it:
- Where you need enforcement, prefer agents that DefenseClaw governs through machine policy (Claude Code, Codex, Cursor, GitHub Copilot and OpenCode).
- Keep
.openhands/hooks.jsonout of the repositories developers use, for example with a repository rule or a server-side push check, and search existing checkouts for it. - Use application control (fapolicyd on Linux, Santa or similar on macOS)
to allow only OpenHands 1.12.0 or later from approved locations,
including copies a package runner such as
uvxstarts from its cache, and to control how users start it (for example only from an administrator-owned launcher that resetsHOME). - Use EDR process protection so users cannot stop or kill the hook process.
See R1, R15, R18 and R30 in the enterprise threat model.
Disable
defenseclaw guardrail disable --connector openhands --yesTeardown removes DefenseClaw-owned hook entries from ~/.openhands/hooks.json or the pinned workspace hook file and leaves unrelated OpenHands hooks untouched.
GitHub Copilot CLI
Copilot CLI connector wires global ~/.copilot/hooks by default, with optional workspace .github/hooks. preToolUse supports native ask; four documented events can block.
Antigravity
Google Antigravity (`agy`) lifecycle hook contract and setup paths for macOS, Linux, and native Windows.