Native harness integrations
Ward supports these project-local integrations:
| ID | Harness | Generated artifacts | Lifecycle coverage |
|---|---|---|---|
codex | OpenAI Codex CLI | .codex/hooks.json, .codex/hooks/ward.sh | pre-tool, post-tool, feedback, approved-artifact guard |
cursor | Cursor | .cursor/hooks.json, .cursor/hooks/ward.sh | pre-tool, post-edit, stop, feedback, approved-artifact guard |
copilot | GitHub Copilot CLI | .github/hooks/ward.json, .github/hooks/ward.sh | pre-tool, post-edit, stop, feedback, approved-artifact guard |
gemini | Gemini CLI | .gemini/settings.json, .gemini/hooks/ward.sh | BeforeTool, AfterTool, AfterAgent feedback, approved-artifact guard |
windsurf | Windsurf/Cascade | .windsurf/hooks.json, .windsurf/hooks/ward.sh | pre-write and pre-command guard; post-write checks shown to you, not the agent |
cline | Cline | .cline/hooks/PreToolUse, .cline/hooks/PostToolUse | pre-tool, post-edit, feedback, approved-artifact guard |
factory-droid | Factory Droid | .factory/hooks.json, .factory/hooks/ward.sh | pre-tool, post-edit, stop, feedback, approved-artifact guard |
amp | Amp | .amp/plugins/ward.ts, .amp/plugins/ward.sh | tool.call guard, tool.result feedback, agent.end continuation |
junie | Junie CLI | .junie/config.json, .junie/hooks/ward.sh | PreToolUse guard for Write, Edit, Bash, apply_patch; turn-end feedback (Stop); approved-artifact guard |
Install or remove one integration with:
ward integrate <agent>ward integrate <agent> --removeGenerated files are project-local, ownership-marked, idempotent, and refreshed when Ward changes.
User-owned files are preserved as conflicts. Most integrations generate a wrapper that takes a phase
argument: ward.sh pre runs the approved-artifact guard (only when it is enabled) and
ward.sh post runs ward hook native --agent <agent> --event-stdin; Cline gets one hook file
per event instead. Keep ward on PATH. Refreshing without a guard option keeps the installed
guard state, including the state of an integration generated by Ward 0.0.7.
What each integration delivers
Section titled “What each integration delivers”Where Ward’s findings end up depends on what the harness lets a hook do. “The agent” means the model receives the findings and can fix them in the same task; “you” means they only appear in the harness UI.
| ID | Guard before a change | Findings after an edit | Findings when the turn ends | Generated-artifact tests |
|---|---|---|---|---|
opencode | Refuses the tool call | The agent, appended to the tool output | The agent, once, as a new prompt | compatibility/opencode-v2 |
claude-code | Denies the tool call | The agent (exit 2) | The agent, once (decision: "block") | compatibility/claude-code |
codex | Denies the tool call | The agent (exit 2) | The agent, once (decision: "block") | compatibility/codex |
copilot | Denies the tool call | The agent (additionalContext) | The agent, once (decision: "block") | compatibility/copilot |
gemini | Denies the tool call | The agent (additionalContext) | The agent, once (turn retry) | compatibility/gemini |
factory-droid | Denies the tool call | The agent (exit 2) | The agent, once (decision: "block") | compatibility/factory-droid |
cursor | Denies the tool call | The agent (additional_context) | The agent, once (followup_message) | compatibility/cursor |
amp | Rejects the tool call | The agent, appended to the tool output | The agent, once (maxContinuations: 1) | compatibility/amp |
junie | Denies the tool call | None: Junie has no post-tool hook | The agent, once (decision: "block") | Pending: compatibility/junie |
cline | Cancels the tool call | The agent (contextModification) | None: Cline ignores turn-end hook output | compatibility/cline |
windsurf | Blocks the write or command | You, in the Cascade UI | None: Cascade has no such hook output | compatibility/windsurf |
The guard is active only when the integration was installed with --guard-approved-specs. Each row is
covered by the generated-artifact tests named in it, which run the real ward against the harness’s
documented contract. That proves the generated files are well formed and reach Ward; it does not
prove that a running harness loads them. ward doctor reports an integration as installed when its
files are present, which likewise says nothing about whether the harness has activated it.
Monorepos
Section titled “Monorepos”For a project below the Git top-level, such as apps/api, the integration installs at the
repository root, where the agent reads it. Its hook names no project, so every nested project shares
the one installation: Ward checks the project whose ward.toml is closest above each edited file,
and at turn end every configured project with changes. See monorepos.
Verification status
Section titled “Verification status”Each adapter is being checked against the pinned harness release’s documented contract
(compatibility/manifest.json). Until an adapter is listed as verified here, treat its generated
configuration as unverified: several adapters were found to write registrations in a shape their
harness does not read.
| ID | Pinned release | Generated-artifact tests | Live run |
|---|---|---|---|
amp | Amp 0.0.1792123242-g03edc7 | Passing (compatibility/amp) | Not run: needs AMP_API_KEY |
codex | Codex CLI 0.163.0 | Passing (compatibility/codex) | Not run: needs OPENAI_API_KEY and a hook-trust bootstrap |
copilot | Copilot CLI 1.0.98 | Passing (compatibility/copilot) | Not run: needs GH_TOKEN with a Copilot subscription |
gemini | Gemini CLI 0.64.0 | Passing (compatibility/gemini) | Not run: needs GEMINI_API_KEY |
factory-droid | Droid 0.243.0 | Passing (compatibility/factory-droid) | Not run: needs FACTORY_API_KEY |
cursor | cursor-agent 2026.10.14-941492e | Passing (compatibility/cursor) | Not run: needs CURSOR_API_KEY |
windsurf | Windsurf 1.126.0 | Passing (compatibility/windsurf) | Witnessed procedure (compatibility/README.md); not yet run |
cline | Cline 3.0.72 | Passing (compatibility/cline) | Not run: needs a model provider and its API key |
junie | Junie CLI, not yet pinned | Pending (compatibility/junie) | Not run: needs a Junie CLI install and credentials |
Every release runs these generated-artifact suites against the native executable it is about to publish (Linux and macOS); a failing suite stops the release.
Generated-artifact tests execute the files that ward integrate writes, with a recording stand-in
for Ward. They prove the generated files have the documented shape and reach Ward correctly,
not that a running harness activates them; only a live run proves that.
Ward writes .codex/hooks.json and the wrapper .codex/hooks/ward.sh. Codex reads hooks.json
strictly, so the ownership marker is stored in its description field. Codex runs project hooks only after
you review and trust them with /hooks; refreshing the integration changes the hooks, so Codex asks again.
| Codex event | Matcher | What happens |
|---|---|---|
PreToolUse | Bash, apply_patch | With the guard enabled, asks Ward about the shell command or patch (tool_input.command) and denies protected changes with permissionDecision: "deny". |
PostToolUse | apply_patch | Runs the quality check. Failures exit 2, so Codex gives the findings to the model. |
Stop | Runs the quality check and spec validation. Findings continue the turn once (decision: "block"); when stop_hook_active is set they are only reported. |
After an edit, the hook runs a partial check for the event’s path, or for every file a patch
adds, updates, deletes, or moves. An event without a recognizable path runs every gate in full. At turn end the
hook runs the full default profile plus spec validation, which also catches files changed by shell commands.
Commands are resolved from the repository root (git rev-parse --show-toplevel), so a session started in a
subdirectory still runs them. Integrations generated by Ward 0.0.7 and earlier were rejected by Codex
because of an unknown top-level field, so no hook ran; refresh with ward integrate codex and trust
the new hooks.
GitHub Copilot CLI
Section titled “GitHub Copilot CLI”Ward writes .github/hooks/ward.json and the wrapper .github/hooks/ward.sh. The events use
their VS Code compatible names, so Copilot sends the tool_name and tool_input payload. The ownership
marker is stored in each handler’s env, since Copilot reads only version and hooks at the top level.
| Copilot event | Matcher | What happens |
|---|---|---|
PreToolUse | bash, edit, create | With the guard enabled, asks Ward about the edited path or shell command and denies protected changes with permissionDecision: "deny". |
PostToolUse | edit, create | Runs the quality check. Copilot treats exit 2 after a tool as a warning, so findings are returned as additionalContext, which the model reads. |
Stop | Runs the quality check and spec validation. Findings continue the turn once (decision: "block"); when stop_hook_active is set they are only reported. |
Checks may take up to 600 seconds (timeoutSec), because Copilot lets a timed-out hook pass. Commands are
resolved from the repository root, so a session started in a subdirectory still runs them. Integrations
generated by Ward 0.0.7 and earlier had no version and no bash command, so Copilot ran none of the
hooks, and their turn-end hook was sessionEnd, whose output Copilot ignores. Refresh with
ward integrate copilot.
Gemini CLI
Section titled “Gemini CLI”Ward writes the hooks into .gemini/settings.json and the wrapper .gemini/hooks/ward.sh. Gemini
registers project hooks only in a trusted folder. The ownership marker is the description of each hook.
| Gemini event | Matcher | What happens |
|---|---|---|
BeforeTool | write_file, replace, run_shell_command | With the guard enabled, asks Ward about tool_input.file_path or tool_input.command and refuses protected changes with decision: "deny". |
AfterTool | write_file, replace | Runs the quality check. Findings are appended to the tool result as additionalContext, so the edit is still reported as applied. |
AfterAgent | Runs the quality check and spec validation. Findings retry the turn once with the findings as the prompt (decision: "block"); when stop_hook_active is set they are only reported. |
Checks may take up to 600 seconds (timeout, in milliseconds). Commands are resolved from the repository
root. Integrations generated by Ward 0.0.7 and earlier wrote AfterTool, AfterAgent and SessionEnd
entries without a hooks list, which Gemini discards, read tool arguments from the wrong fields, and
returned turn-end findings as plain text, which Gemini does not treat as a retry. Refresh with
ward integrate gemini.
Junie CLI
Section titled “Junie CLI”Ward writes the hooks into .junie/config.json and the wrapper .junie/hooks/ward.sh, at the
repository root. Junie ignores a project’s .junie/config.json unless it starts with that file
passed explicitly, so launch Junie with:
junie --config-location .junie/config.jsonWithout this flag Junie runs no Ward hook, and Ward cannot report that it is missing.
| Junie event | Matcher | What happens |
|---|---|---|
PreToolUse | Write, Edit, Bash, apply_patch | With the guard enabled, refuses protected changes with exit status 2 and Ward’s reason on stderr. Junie ignores a JSON deny on stdout, so Ward does not use one. Allowed calls run no quality check. |
Stop | Runs the quality check on the projects the turn changed. Findings block the stop once with decision: "block", so Junie retries the turn with them. |
Junie has no post-tool event, so Ward reports no post-edit capability: findings arrive only when the
turn ends, not after each edit. A Stop check may take up to 600 seconds (timeout, in seconds).
Junie stops calling the Stop hook after 8 consecutive blocks in one task, so a long failing loop ends
without further retries. Set JUNIE_STOP_HOOK_BLOCK_CAP to change the cap.
The Write and Edit payload fields that the guard reads (tool_input.file_path), and the apply_patch tool_input.patch text, are assumed to
match Claude Code’s format. Junie’s documentation does not list them, and the live capture that
confirms them is pending, so the guard for those tools is unverified until it runs.
Factory Droid
Section titled “Factory Droid”Ward writes .factory/hooks.json and the wrapper .factory/hooks/ward.sh. Droid validates the whole
hook file and loads nothing from a file that doesn’t match its schema, so the ownership marker is a shell
comment at the end of each command.
| Droid event | Matcher | What happens |
|---|---|---|
PreToolUse | Create, Edit, MultiEdit, ApplyPatch, Execute | With the guard enabled, asks Ward about tool_input.file_path, the patch in tool_input.input, or tool_input.command, and denies protected changes with permissionDecision: "deny". |
PostToolUse | Create, Edit, MultiEdit, ApplyPatch | Runs the quality check. Failures exit 2, so Droid gives the findings to the model. |
Stop | Runs the quality check and spec validation. Findings continue the turn once (decision: "block"); when stop_hook_active is set they are only reported. |
Checks may take up to 600 seconds (timeout). Integrations generated by Ward 0.0.7 and earlier wrote
.factory/plugins/ward.json, which Droid doesn’t read, so no hook ran. Refresh with
ward integrate factory-droid, then delete the old .factory/plugins/ward.json and
.factory/plugins/ward.sh.
Cursor
Section titled “Cursor”Ward writes .cursor/hooks.json and the wrapper .cursor/hooks/ward.sh. Cursor runs project hooks
from the project root, in the editor and in cursor-agent. File changes reach hooks as the Write and
Delete tools; shell commands as Shell.
| Cursor event | Matcher | What happens |
|---|---|---|
preToolUse | Shell, Write, Delete | With the guard enabled, asks Ward about tool_input.file_path or the shell command (in its working_directory) and refuses protected changes with permission: "deny". |
postToolUse | Write, Delete | Runs the quality check. Findings are returned as additional_context, which Cursor adds to the conversation. |
stop | Runs the quality check and spec validation. Findings are submitted once as the next prompt (followup_message); once loop_count is above zero they are only reported. |
Integrations generated by Ward 0.0.7 and earlier had no version and a matcher group Cursor doesn’t
accept, so Cursor rejected the file, and they used afterFileEdit, whose output Cursor ignores. Refresh
with ward integrate cursor.
Windsurf
Section titled “Windsurf”Ward writes .windsurf/hooks.json and the wrapper .windsurf/hooks/ward.sh. Cascade runs the hooks
from the workspace root. The ownership marker is a shell comment at the end of each command.
| Cascade event | What happens |
|---|---|
pre_write_code | With the guard enabled, asks Ward about tool_info.file_path. A protected change exits 2, which blocks the write; Cascade shows the agent the reason. |
pre_run_command | With the guard enabled, asks Ward about tool_info.command_line in its cwd, and blocks protected commands the same way. |
post_write_code | Runs the quality check. Findings appear in the Cascade UI (show_output). |
Windsurf can’t pass hook output to the agent after a write or at the end of a response, so the agent does
not receive quality findings; you see them and can ask Cascade to fix them. Integrations generated by
Ward 0.0.7 and earlier had no guard and read neither the file path nor the command, so their checks
ignored what was edited. Refresh with ward integrate windsurf.
Cline 3 runs the files in .cline/hooks/ that are named after an event. Ward writes two of them, each
with its ownership marker.
| Hook file | Tools | What happens |
|---|---|---|
PreToolUse | editor, apply_patch, run_commands | With the guard enabled, asks Ward about the edited path, the patch, or each command, and stops protected changes with cancel: true and the reason. |
PostToolUse | editor, apply_patch | Runs the quality check. Findings are returned as contextModification, which Cline appends to the tool result. |
Cline runs TaskComplete hooks in the background and ignores their output, so Ward has no turn-end
check for Cline. Integrations generated by Ward 0.0.7 and earlier wrote .cline/hooks/ward.json
and .cline/hooks/ward.sh, which Cline doesn’t run. Refresh with ward integrate cline, then
delete those two files.
Ward writes the plugin .amp/plugins/ward.ts and the wrapper .amp/plugins/ward.sh.
Amp loads project plugins only from the binary-installed Amp CLI (not the npm package or the editor
extension), and only when started with PLUGINS=all. In execute mode, add --plugin-ready-timeout so
the turn waits for plugins to load. Plugins run with your privileges; only enable them in trusted
workspaces.
| Amp event | What the plugin does |
|---|---|
tool.call | For each file the call would modify, and for shell commands, asks the guard. A refusal returns reject-and-continue with Ward’s reason. If Ward can’t decide, the call is allowed and the reason is logged. |
tool.result | After a successful call that modified files, runs the quality check (and spec validation for edited OpenSpec files). Failures are appended to the tool output the agent reads. Failed or cancelled calls are left untouched. |
agent.end | When the turn ends with findings, continues the turn once with the findings (maxContinuations: 1), so an unfixable finding can’t loop. |
Plugins generated by Ward 0.0.7 and earlier registered these events without calling Ward, so
no checks or guard decisions ran. Refresh with ward integrate amp, then restart Amp so it reloads
the plugin. To roll back, run ward integrate amp --remove; it deletes only Ward-owned files.
Capabilities are reported by integrate and doctor. Post-event-only harnesses are reported as
feedback-only; Ward does not claim pre-tool blocking where the native lifecycle cannot block.
Each native adapter parses its own payload aliases and maps its own lifecycle events into Ward’s
normalized hook model. Denials are serialized using the harness response envelope: Codex uses
hookSpecificOutput, Copilot uses permissionDecision, Cursor uses permission, Gemini uses decision, Cline uses cancel, Windsurf uses exit code 2, and Amp uses
reject-and-continue; the remaining advisory-compatible adapters use their documented decision
shape. Invalid or oversized payloads are treated as unreadable and never echoed in diagnostics.
Generated artifacts contain a Ward ownership marker. Re-running integration refreshes only Ward-owned artifacts; unmarked files are reported as conflicts and remain unchanged. Removal likewise deletes only owned configuration and wrapper files, preserving unrelated project content.