Runtime overview
Asymptote supports OpenHands — the open-source coding agent from All Hands — through its native hooks, sent tobeacon-hooks.
--harness open-hands is also accepted.
OpenHands runs the same agent in three places — Cloud, the CLI, and the local GUI — and all three read hooks from the same .openhands/hooks.json. There is one integration rather than three.
OpenHands LM is the model family; OpenHands is the agent. Asymptote hooks the agent, so the harness name is
openhands. OpenHands LM spellings are deliberately not accepted as harness aliases and are not mapped to the harness name: every one of those model ids begins with the same word the harness does, so a substring rule would file any event carrying one under the agent, and a reader could not tell that from a real session.Prerequisites
Before enabling OpenHands telemetry, make sure:- OpenHands is installed and available as the
openhandsexecutable, or you are running the SDK directly. beacon endpoint installhas run so shared endpoint config and runtime log paths exist.- OpenHands is restarted after hook install or removal so new conversations load the updated registration.
Collection path
OpenHands registers hooks in one JSON file that you also own, so Asymptote merges its commands into it rather than writing a file of its own. Hooks you already have are preserved exactly, including their matchers, timeouts, and any fields Asymptote has no model for.$OH_PERSISTENCE_DIR replaces ~/.openhands when it is set, matching how OpenHands resolves the directory itself. Sandboxed and containerized setups use it to move state onto a volume.
beacon endpoint inventory reports both scopes, so a user-level install shadowed by a repository file is visible as that rather than as a working one.
Registered events
OpenHands exposes six lifecycle events and Asymptote registers all six. Nothing is left out — this is the runtime’s whole hook surface.
Hooks are registered synchronously rather than with
"async": true. An async hook is fire-and-forget with its stdout discarded, which would make the policy seam unable to deny on the one event that can block.
The file format fails silently
hooks.json has a closed schema, and every way of getting it wrong ends with hooks silently not running rather than with an error you see. Asymptote’s installer is shaped around that, and each of these is covered by a test:
- The top level is the event map itself, and unknown keys are fatal. One unrecognized top-level key fails validation and the whole file loads as nothing. That is why Asymptote writes no marker key here, unlike the files it owns on other runtimes: an install is recognized by the hook command it contains.
- A legacy
{"hooks": {...}}wrapper is accepted, and the loader unwraps it by replacing the whole document. Adding a top-level event beside an existing wrapper therefore does not merge — it discards the added hooks. Asymptote writes inside the wrapper when it finds one. - Event names may be snake_case or Claude Code’s PascalCase, and naming one event both ways is fatal to the entire file. Asymptote writes under whichever spelling the file already uses for an event and never introduces a second one, so a
hooks.jsonshared with Claude Code keeps its PascalCase names.
The hook environment
OpenHands runs hook commands through a shell with the conversation’s working directory as the cwd, and exportsOPENHANDS_EVENT_TYPE, OPENHANDS_TOOL_NAME, OPENHANDS_PROJECT_DIR, and OPENHANDS_SESSION_ID.
Asymptote passes the endpoint log and config locations to beacon-hooks as --log and --config flags rather than through the environment, so a hook finds them wherever it runs. OPENHANDS_PROJECT_DIR is read as a fallback for the workspace path, because the payload’s working_dir is optional and null when the SDK was given no working directory.
Discovery and status
Show OpenHands hook status
openhands harness with capability hooks, and every event carries harness.collection_method=hook: the events, the payload shapes, and the hooks.json contract are all the vendor’s, and Asymptote ships no plugin source of its own for this runtime.
Install or configuration support
beacon endpoint install prepares shared endpoint config and runtime log paths. OpenHands hooks are installed separately, because they live in a repository or a user config directory rather than in collector service configuration:
.openhands/hooks.json if you want the hooks to apply to everyone working in the repository, the same way the OpenHands documentation describes for hooks you write yourself.
Re-running the install replaces Asymptote’s own hooks rather than adding a second copy, so a repair does not end with the binary firing twice per event. Uninstall removes what Asymptote added and nothing else; a file that still holds your own hooks is rewritten without Asymptote’s, and a file left with nothing in it is removed rather than left behind as an empty stub.
Telemetry coverage
Tool taxonomy
OpenHands sends the Claude Code payload shape but its own tool names, and one of them cannot be classified by name at all:file_editor is the reason this table exists. It multiplexes five operations onto one tool name and selects between them with a command argument, so a classifier that only sees the name is wrong on one of view/str_replace whichever way it answers — and that is misclassification rather than absence, which is the worse failure: the event is there, the action is wrong, and nothing looks broken. A file_editor call whose command did not arrive is recorded as tool.invoked, which says a tool ran and nothing about what it did to the file.
Diffs come from the file, not from the request
Every other runtime’s diff is reconstructed from what the model asked for — anold_string/new_string pair, a patch body, a full-file write — and the reconstruction is only as right as the assumption that the tool did exactly what it was told.
OpenHands reports the file’s whole content before and after on the observation, so nothing has to be inferred. An insert at line 40 and a single-occurrence replace both come back as the same two strings and produce a diff that matches what is on disk. An apply_patch that applied with fuzz records what landed, not what was requested.
Diffs are captured for the file types Asymptote scans, subject to the usual retention, redaction, and size controls. An edit to a file outside that set is still recorded as file.modified with its path — it simply carries no diff.
A failed edit is never recorded as an edit. The editor reports a failed str_replace with the same tool name and the same arguments as a successful one, so without that guard a diff would be built from a write that never landed and the log would assert a file changed when it did not.
Approvals and enforcement
Asymptote records no approval decisions for OpenHands, and does not synthesize any. OpenHands exposes no approval hook:pre_tool_use announces a tool call the agent is about to make, not a question anybody was asked, and its own confirmation mode is not surfaced to hooks. Recording an approval.allowed from it would put an operator decision in the log that no operator made — the same call already settled for Cline, Pi, and fx.
Asymptote’s pre_tool_use hook answers with an empty object. OpenHands reads decision, reason, additionalContext, and continue from a hook’s stdout; a speculative allow carries none of those and is inert today, but the raw string is shown in the conversation as a decision Asymptote did not make, and would begin taking effect if OpenHands ever read that key.
Enforcement stays behind the optional, off-by-default policy provider seam (BEACON_POLICY_PROVIDER), which is inert unless it names an executable and fails open on any error. OpenHands is one of the runtimes where a provider deny does take effect: pre_tool_use can block, and Asymptote answers a deny with {"decision": "deny", "reason": "..."}. The reason is sent because OpenHands surfaces it in the conversation as the explanation for the block and hands it to the agent as the tool’s failure — without it, the operator and the model both see a call refused with no account of why.
Data handling
OpenHands content is handled like every other runtime. Prompts, tool arguments, commands, command output, paths, and diffs are retained in local or customer-controlled logs, with local secret redaction and per-string limits applied before writing, and a hash and byte count on each content-bearing event. Events over the 64 KiB limit drop raw and retained content, preserve stable metadata, and setfield_truncated.
Unlike Qwen Code and Muse Code, Asymptote does not preserve the verbatim OpenHands payload under a raw key. The payload’s only field with no schema home is metadata, which is empty on five of the six events and carries a constant reason on stop. Keeping the payload would cost the whole tool_input and tool_response on every event — file contents included, twice over on an edit — to preserve a value that never varies.
Image parts in a tool result are skipped rather than described: they carry base64 data URLs, often megabytes of them, and writing one into an event field would be worse than recording nothing.
Known gaps
- No approval decisions. OpenHands has no approval or permission hook, and Asymptote does not synthesize one from
pre_tool_use. Approval rules and dashboards will show nothing for this runtime. - No token usage or cost. None of the six hook payloads carries token counts or cost, and OpenHands exposes no other hook that would. OpenHands activity therefore does not appear in
beacon token-usagereports or the dashboard token view. - No tool call identity. The hook payload carries no per-call identifier, so
gen_ai.tool.call.idis absent and a pre-tool event cannot be joined to its post-tool event by id. TheHookEventmodel has no field for it; the id exists on the conversation’s action event, which hooks are not given. - MCP calls are only identifiable after the fact. OpenHands calls an MCP tool by whatever name its server gave it, with no prefix and no
mcp_*argument, so the only thing that says a call went to an MCP server is the result it came back with. Apre_tool_useevent for an MCP tool is recorded under the action its name implies. The MCP server name is not exposed on the result at all, somcp.serveris empty. - No transcript path. Conversation state lives in the OpenHands persistence directory and no payload field points at it, so nothing on this path reads it.
- No compaction or subagent events. OpenHands emits neither on a hook. Its
Stophook is a turn boundary, not a session end; the session-end event is separate and Asymptote registers both. - A user-scope install is shadowed by a repository’s own hooks file, and invisible to the agent server. See the warning under Collection path. This is OpenHands’ loader behaviour, not something Asymptote can work around.
- Assistant response text is not collected. No hook payload carries it.
Deployment notes
Hooks are loaded when a conversation starts, so an install taken while OpenHands is running takes effect on the next conversation. Restart OpenHands, or start a new conversation, after installing or removing hooks. Confirm status, then generate one OpenHands event and check the log:- Confirm you installed at the scope the runtime reads. The CLI, the GUI, and Cloud read only
<repo>/.openhands/hooks.json. - Confirm no other
.openhands/hooks.jsonis shadowing the one you installed. OpenHands takes the first it finds and does not merge. - Confirm the file contains commands with
--platform openhands. - Check
$OH_PERSISTENCE_DIR: if it is set, the user-scope file is under that directory, not~/.openhands. - Confirm hook timeouts are second values, not milliseconds.
- In the CLI, run
/skillsto see the skills, hooks, and MCPs loaded for the current session. - Start a new conversation so the updated registration is loaded.
Related
Supported agent harnesses
Return to the runtime support overview.
Agent harness integration model
See how Asymptote discovers and configures each runtime.

