Adding a new agent harness
July 27, 2026 · View on GitHub
This recipe names every step to wire a new agentic runtime into Magpie so it loads skills, enforces the action guard, and runs the spec-loop. It is the counterpart to the tool-adapter authoring guide — that guide adds a new backend (forge, VCS, mail system); this guide adds a new agent runtime.
The framework's skills are plain
AGENTS.md-standard Markdown. No runtime
gets its own copy or variant of a skill — the content is
byte-identical. What differs per runtime is:
- Where on disk it looks for skills (the symlink relay).
- Whether it enforces the action guard (the pre-tool hook).
- How the spec-loop invokes it headlessly (the
--cliprofile). - Which substrate tools list it (the
**Harness:**declarations).
Steps 1 and 3 are always required. Step 2 is required for runtimes that support a pre-tool hook API (and a matter of OS-level fallback for those that do not). Step 4 is a metadata update, not executable code.
Step 1 — add a row to the agent-target registry
The single source of truth for where each runtime reads skills from is
skills/setup/agents.md. Add one row:
| `<id>` | `.<runtime>/skills/` | native (relay) | <Runtime name> |
<id>— a short, lowercase, hyphen-safe identifier (e.g.kiro,aider,openhands).<runtime>/skills/— the path the runtime reads for project-scope skills. Check the runtime's docs; if it reads.agents/skills/directly (like Codex, Cursor, Gemini CLI) no new row is needed — theuniversalrow already covers it.native (relay)— used for runtimes with their own per-agent directory. Leave the kind column asnative (relay)if the directory is distinct from.agents/skills/; useuniversalonly for runtimes that read.agents/natively.
Once the row is present, /magpie-setup adopt and upgrade wire the
relay symlinks automatically. The rules are:
- The canonical entry lives at
.agents/skills/magpie-<skill>and points at the skill source. - Every relay entry at
.<runtime>/skills/magpie-<skill>points at../../.agents/skills/magpie-<skill>— through the canonical, not straight at source.
symlink-lint enforces both rules and is wired into prek; run it to
verify after any manual link creation:
uv run --project tools/symlink-lint python3 -m symlink_lint
Step 2 — wire the action guard
The action guard (tools/agent-guard) intercepts shell commands before
they run and denies ones that break hard framework rules (unauthorized
git push, wrong commit-trailer format, premature --ready-for-review,
…). The guard decisions live in one harness-agnostic dispatch() core;
each harness gets a thin adapter translating its hook format.
For runtimes with a pre-tool hook API: add an adapter following the existing OpenCode example.
tools/agent-guard/
src/agent_guard/__init__.py ← dispatch() core (harness-agnostic)
src/agent_guard/__main__.py ← CLI entry point (stdin JSON → stdout JSON)
src/agent_guard/guards.d/ ← individual guard scripts
opencode/plugin.js ← OpenCode adapter (JS plugin)
tests/ ← test suite incl. per-harness tests
The core (__init__.py) exposes a dispatch() function. Per-harness
adapters translate the runtime's hook format into a dispatch() call.
Your adapter must:
- Parse the harness's pre-tool hook payload (stdin JSON, env vars, CLI args, or a native plugin format — check the runtime docs).
- Call
dispatch(command_string)from the core module. - Return the harness's expected "allow" or "block" response.
Run the guard's test suite to verify the adapter:
uv run --project tools/agent-guard --group dev pytest
For runtimes without a hook API: document the limitation explicitly
in the row's notes in skills/setup/agents.md. An OS-level wrapper
(e.g. bubblewrap on Linux, sandbox-exec on macOS, or a custom
PATH-shadowing script that intercepts git push and gh pr create)
can enforce the rules at the process level without a harness hook. The
credential-strip layer is already available for any runtime:
# Source agent-iso.sh and use the generic entry point:
source /path/to/magpie/tools/agent-isolation/agent-iso.sh
agent-iso <your-runtime-cli> [cli-args]
See tools/agent-isolation/README.md
for the full generic-harness usage. The agent-iso entry point works
for any CLI; the Claude-specific --settings sandbox grant is skipped
automatically. For deeper enforcement (action guard, confirmation
prompts), a harness-specific adapter is still needed.
Whatever the enforcement mechanism, update the affected substrate tool
READMEs (step 4) to declare the new harness in their **Harness:** line
once support lands.
Step 3 — add a spec-loop runner profile
The spec-loop (tools/spec-loop/loop.sh) runs the build/plan/update/
consolidate beats headlessly. Each runtime has a different flag for
"accept all permissions and run non-interactively." Add your runtime's
convention in two places.
tools/spec-loop/loop.sh — harness detection:
case "${SPEC_LOOP_HARNESS:-$(basename "$AGENT")}" in
*codex*) HARNESS=codex ;;
*cursor*) HARNESS=cursor ;;
*gemini*) HARNESS=gemini ;;
*opencode*) HARNESS=opencode ;;
*<runtime>*) HARNESS=<runtime> ;; # add your case here
*) HARNESS=claude ;;
esac
tools/spec-loop/lib.sh — headless invocation:
Add a branch inside spec_loop_launch_agent() following the existing
patterns. Each branch runs the agent in the background (&) with:
- the auto-approve / skip-permissions flag for that runtime;
--model "$model"forwarded when non-empty;SPEC_LOOP_EFFORTmapped when the CLI has a reasoning-effort knob (see the effort table intools/spec-loop/specs/spec-loop-runner.md); if the CLI has no such flag, omit silently like Cursor/Gemini — do not warn;- the prompt fed via stdin, a file argument, or a flag — whatever the runtime accepts.
Example template:
elif [ "$harness" = "<runtime>" ]; then
local effort_args=()
case "$effort" in
low) effort_args=(--<effort-flag> <low-value>) ;;
medium) effort_args=(--<effort-flag> <medium-value>) ;;
high) effort_args=(--<effort-flag> <high-or-top-value>) ;;
esac
"$agent" <headless-flag> \
${model_args[@]+"${model_args[@]}"} \
${effort_args[@]+"${effort_args[@]}"} \
"$(cat "$prompt_file")" &
Take the flag name and accepted values from the runtime's own --help,
record them in the effort matrix, and map framework high to the CLI's
top sensible value when it offers more levels than low|medium|high.
Validate the loop syntax after editing:
bash -n tools/spec-loop/loop.sh && bash -n tools/spec-loop/lib.sh
If the runtime supports a structured-output or JSON-stream mode
equivalent to Claude's --output-format stream-json, wire it via an
$output_format check (see the Cursor branch for an example).
Step 4 — update tool harness declarations
Substrate tools that are harness-coupled declare **Harness:** in their
README. When your new runtime uses one of these tools, update its line:
| Tool | Current harness declaration |
|---|---|
tools/agent-guard | **Harness:** Claude Code, OpenCode |
tools/agent-isolation | **Harness:** agnostic |
tools/permission-audit | **Harness:** Claude Code, OpenCode |
tools/sandbox-lint | **Harness:** Claude Code, OpenCode |
tools/spec-loop | **Harness:** Claude Code, Codex, Cursor, Gemini CLI, OpenCode |
tools/agent-isolation already exposes a harness-agnostic agent-iso <cli> entry
point — no update needed for new runtimes (see
tools/agent-isolation/README.md).
Add your runtime to every other tool it integrates with. Tools that declare
**Harness:** any or agnostic need no change — they work under any runtime
unchanged. After updating, refresh the vendor-neutrality score:
uv run --project tools/vendor-neutrality-score vendor-neutrality-score --markdown
Copy the generated block into docs/vendor-neutrality.md as instructed
in its comment header.
Step 5 — validate the full wiring
# 1. Symlink topology (no cycles, correct relay direction)
uv run --project tools/symlink-lint python3 -m symlink_lint
# 2. Skill + tool metadata (capability, prerequisites, organization lines)
uv run --project tools/skill-and-tool-validator --group dev skill-and-tool-validate
# 3. Guard tests (if you added a guard adapter in step 2)
uv run --project tools/agent-guard --group dev pytest
# 4. Loop syntax
bash -n tools/spec-loop/loop.sh && bash -n tools/spec-loop/lib.sh
All four must pass before opening a PR.
Scope of a first-class harness integration
A minimal integration lands steps 1 + 3: skills load, the loop runs. Steps 2 and 4 are what turn "skills load" into "skills run safely." The recommended order for a community contribution:
- Open a tracking issue (or claim the existing one for your
runtime — see
docs/vendor-neutrality.md§ Agentic runtime for the issue list). - Land the registry row + relay symlinks (step 1) — small PR, unblocks users immediately.
- Land the loop profile (step 3) — small PR, enables spec-loop use.
- Land the guard adapter or OS-level wrapper (step 2) — the most harness-specific work; can be a follow-up PR.
- Update harness declarations (step 4) — include in whichever PR lands the first substantive integration point.
This matches how Claude Code and OpenCode integrations were built: the skill path landed first, then guard, then loop, then full harness table.
See also
skills/setup/agents.md— the agent-target registry (single source of truth for symlink paths).tools/symlink-lint/— enforces relay symlink correctness.tools/agent-guard/— the action guard core and per-harness adapters.tools/agent-isolation/— the clean-environment launcher (OS-level complement to the agent hook).tools/spec-loop/loop.shandlib.sh— the spec-loop runner with per-harness invocation profiles.docs/vendor-neutrality.md§ Agentic runtime — the open tracking issues per runtime and the neutrality rationale.docs/adapters/authoring.md— the analogous recipe for adding a new tool backend (forge, VCS, mail archive, CNA).