OpenHands (openhands)
Set up this tool
OpenHands gets AGENTS.md, agents and skills under .agents/, hooks, and a setup script. Add MCP servers in OpenHands itself.
First rule
Select openhands in agnostic-ai.yaml, then follow First rule to add the shared commit-prefix rule and sync it. With the default paths, it reaches AGENTS.md.
MCP servers have no output. Add them in Agent Canvas, as MCP describes.
For existing files, check what import openhands reads before syncing.
Verify
Run agnostic-ai sync --check and inspect AGENTS.md for the commit-prefix rule. Open the project in OpenHands and ask for a commit subject for a small fix; it should use fix:.
Next, add a skill.
Output
AGENTS.md # pointer body + inlined always-on rules (shared path)
.agents/agents/<name>.md # one project agent (shared with Goose)
.agents/skills/<name>/SKILL.md # one folder per skill or path-triggered rule (shared tree)
.openhands/hooks.json # when hook entries exist
.openhands/setup.sh # when an environment spec sets `install`
Native mappings
Field mappings, discovery, and compatibility
Skills
Written identically to the other .agents/skills/ tools, so the shared folder is written once.
The OpenHands skills docs document no field that keeps a skill user-only. The SDK reads disable-model-invocation, but such a skill then runs only when a triggers keyword appears anywhere in a message, not by name. Sync does not write it: disable-model-invocation is dropped with a note, and commands are skipped.
Agents
OpenHands loads .agents/agents/<name>.md in local conversations. The writer is shared with Goose and writes name, description, an optional free-form model, and the prompt body.
- Each server an agent names in
mcpServersis written in full undermcp_servers, the only form OpenHands takes. Goose has no such key, so while Goose writes the same folder sync writes no servers and prints a note; setoutputs.openhands.agents-dirto.openhands/agents.x-openhands.mcp_serversreplaces the list. - A generic
toolslist is dropped with a sync note, because OpenHands uses its own tool names (file_editor,terminal). Setx-openhands.toolswith those names instead. For an OpenHands profile that differs from Goose's, pointoutputs.openhands.agents-dirat.openhands/agents. - A portable
coloris dropped with a sync note, because Goose's agent frontmatter has no color. Setx-openhands.colorto a Rich color name to write it.
Rules
An always-on rule goes into the ## Rules block of AGENTS.md. So does a rule with a catch-all globs or paths. A rule is always-on when it has no globs or paths and no scope from its folder or frontmatter.
Path-triggered rules
A scoped rule becomes .agents/skills/<name>/SKILL.md with a paths: list (its scope directory plus file patterns). OpenHands loads it only when a matching file is touched. It uses the folder form (a flat .md also works), so these rules share outputs.openhands.skills-dir with skills.
Hooks
OpenHands reads .openhands/hooks.json (outputs.openhands.hooks-file) per repository in Agent Canvas, OpenHands Cloud, and OpenHands Enterprise. agnostic-ai hook run tests them before a session, using OpenHands's event data, shell, and timeout.
Hooks load only from the working folder, so a root .openhands/hooks.json does not apply when OpenHands works in a subfolder.
- Six events:
PreToolUse,PostToolUse,UserPromptSubmit,Stop,SessionStart,SessionEnd. - Sync writes the Claude form (PascalCase keys inside a
{"hooks": {...}}wrapper). OpenHands also accepts its own unwrapped snake_case form. - Each entry has
command,type(alwayscommand), and optionaltimeout(seconds, default 60) andasync.matcherapplies only to the two ToolUse events. - A Claude matcher such as
Bashmatches nothing, because OpenHands calls that toolterminal. Sync adds a note and does not rename it. OpenHands documents onlyterminal,*, and regex.
MCP
No file. OpenHands keeps MCP servers in its settings store: add them in Agent Canvas under Customize > MCP Servers, or pass mcp_config to the SDK. It reads no project file, and ignores the [mcp] section of a project config.toml (legacy V0).
- A project or home MCP spec gets one sync note that points at Customize > MCP Servers.
- An agent's
mcpServersstill reach itsmcp_servers, as Agents describes. There a remoteapi_keybecomes anAuthorization: Bearer <key>header, as OpenHands sends it. AnAuthorizationentry inheaderswins. timeoutgets a sync note: sync does not map it into an agent'smcp_servers. Agent Canvas separately offers an optional SHTTP timeout in seconds in its server editor.
Environments
install writes .openhands/setup.sh, the repository setup script OpenHands runs each time it opens the repo.
- The file has a
#!/bin/bashline, the generated-file header, theninstallas written. OpenHands runschmod +xitself, so sync does not set the executable bit. terminals(Cursor's long-running dev processes) gets a sync note, because the script runs once when the repo starts.- With several environment specs, the last
installwins, as for Cursor.
Legacy config.toml and mcp.json
The next sync removes a config.toml that an earlier release wrote. import openhands still reads the [mcp] table of a legacy config.toml into specs.
Only the unmaintained OpenHands-CLI reads ~/.openhands/mcp.json. sync --global no longer writes it. The next sync --global removes the servers sync recorded as its own there. That includes a hand-written server an earlier sync adopted because it matched a spec exactly. Sync also removes the file when it created it and nothing else is left. Other servers stay. Run sync --global --backup to keep a copy of the file first. import --global openhands still reads the file into specs.
Config keys
| Key | Default |
|---|---|
outputs.openhands.agents-dir | .agents/agents |
outputs.openhands.skills-dir | .agents/skills |
outputs.openhands.hooks-file | .openhands/hooks.json |
outputs.openhands.setup-file | .openhands/setup.sh |
outputs.openhands.mcp-file no longer has an effect.
Import
agnostic-ai import openhands reads the OpenHands files back into specs:
| Source | Becomes |
|---|---|
AGENTS.md embedded ## Rules block (### <name> children) | <rules>/<name>.md per rule |
.agents/skills/<name>/SKILL.md with paths (path-triggered rule) | <rules>/<name>.md with paths |
.agents/skills/<name>/SKILL.md (+ bundled files) | <skills>/<name>/SKILL.md |
.openhands/skills/ and .openhands/microagents/ (legacy) | the same, read after .agents/skills/ |
flat <name>.md in any of those three directories | a rule, or a skill when it has triggers (kept under x-openhands) |
.agents/agents/<name>.md | <agents>/<name>.md, with native skills under x-openhands |
.openhands/hooks.json | one hook spec per matcher group |
config.toml [mcp] table (legacy V0) | <mcps>/<name>.yaml per server |
.openhands/setup.sh | <environments>/openhands-setup.yaml with the script as install |
AGENTS.md | .agnostic-ai/AGNOSTIC_AI.md |
.agents/skills/ wins a name clash with the legacy folders, as in OpenHands. Hooks import from both layouts. Snake_case pre_tool_use comes back as PreToolUse.
Import loses these details (none change what OpenHands loads):
- Unnamed
sse_serversandshttp_serversentries are named after the URL host (docs-example-test). - A rule scoped by its folder comes back as the
pathsglob it was widened to. - An environment spec keeps only
install. Its name andterminalsare lost.
Protected paths
Not enforced. This target takes no settings specs, so sync reports a spec with a protected block as unsupported. See protected-path alternatives.