Skip to content

Native harness integrations

Ward supports these project-local integrations:

IDHarnessGenerated artifactsLifecycle coverage
codexOpenAI Codex CLI.codex/hooks.json, .codex/hooks/ward.shpre-tool, post-tool, feedback, approved-artifact guard
cursorCursor.cursor/hooks.json, .cursor/hooks/ward.shpre-tool, post-edit, stop, feedback, approved-artifact guard
copilotGitHub Copilot CLI.github/hooks/ward.json, .github/hooks/ward.shpre-tool, post-edit, stop, feedback, approved-artifact guard
geminiGemini CLI.gemini/settings.json, .gemini/hooks/ward.shBeforeTool, AfterTool, AfterAgent feedback, approved-artifact guard
windsurfWindsurf/Cascade.windsurf/hooks.json, .windsurf/hooks/ward.shpre-write and pre-command guard; post-write checks shown to you, not the agent
clineCline.cline/hooks/PreToolUse, .cline/hooks/PostToolUsepre-tool, post-edit, feedback, approved-artifact guard
factory-droidFactory Droid.factory/hooks.json, .factory/hooks/ward.shpre-tool, post-edit, stop, feedback, approved-artifact guard
ampAmp.amp/plugins/ward.ts, .amp/plugins/ward.shtool.call guard, tool.result feedback, agent.end continuation
junieJunie CLI.junie/config.json, .junie/hooks/ward.shPreToolUse guard for Write, Edit, Bash, apply_patch; turn-end feedback (Stop); approved-artifact guard

Install or remove one integration with:

Terminal window
ward integrate <agent>
ward integrate <agent> --remove

Generated 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.

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.

IDGuard before a changeFindings after an editFindings when the turn endsGenerated-artifact tests
opencodeRefuses the tool callThe agent, appended to the tool outputThe agent, once, as a new promptcompatibility/opencode-v2
claude-codeDenies the tool callThe agent (exit 2)The agent, once (decision: "block")compatibility/claude-code
codexDenies the tool callThe agent (exit 2)The agent, once (decision: "block")compatibility/codex
copilotDenies the tool callThe agent (additionalContext)The agent, once (decision: "block")compatibility/copilot
geminiDenies the tool callThe agent (additionalContext)The agent, once (turn retry)compatibility/gemini
factory-droidDenies the tool callThe agent (exit 2)The agent, once (decision: "block")compatibility/factory-droid
cursorDenies the tool callThe agent (additional_context)The agent, once (followup_message)compatibility/cursor
ampRejects the tool callThe agent, appended to the tool outputThe agent, once (maxContinuations: 1)compatibility/amp
junieDenies the tool callNone: Junie has no post-tool hookThe agent, once (decision: "block")Pending: compatibility/junie
clineCancels the tool callThe agent (contextModification)None: Cline ignores turn-end hook outputcompatibility/cline
windsurfBlocks the write or commandYou, in the Cascade UINone: Cascade has no such hook outputcompatibility/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.

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.

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.

IDPinned releaseGenerated-artifact testsLive run
ampAmp 0.0.1792123242-g03edc7Passing (compatibility/amp)Not run: needs AMP_API_KEY
codexCodex CLI 0.163.0Passing (compatibility/codex)Not run: needs OPENAI_API_KEY and a hook-trust bootstrap
copilotCopilot CLI 1.0.98Passing (compatibility/copilot)Not run: needs GH_TOKEN with a Copilot subscription
geminiGemini CLI 0.64.0Passing (compatibility/gemini)Not run: needs GEMINI_API_KEY
factory-droidDroid 0.243.0Passing (compatibility/factory-droid)Not run: needs FACTORY_API_KEY
cursorcursor-agent 2026.10.14-941492ePassing (compatibility/cursor)Not run: needs CURSOR_API_KEY
windsurfWindsurf 1.126.0Passing (compatibility/windsurf)Witnessed procedure (compatibility/README.md); not yet run
clineCline 3.0.72Passing (compatibility/cline)Not run: needs a model provider and its API key
junieJunie CLI, not yet pinnedPending (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 eventMatcherWhat happens
PreToolUseBash, apply_patchWith the guard enabled, asks Ward about the shell command or patch (tool_input.command) and denies protected changes with permissionDecision: "deny".
PostToolUseapply_patchRuns the quality check. Failures exit 2, so Codex gives the findings to the model.
StopRuns 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.

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 eventMatcherWhat happens
PreToolUsebash, edit, createWith the guard enabled, asks Ward about the edited path or shell command and denies protected changes with permissionDecision: "deny".
PostToolUseedit, createRuns the quality check. Copilot treats exit 2 after a tool as a warning, so findings are returned as additionalContext, which the model reads.
StopRuns 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.

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 eventMatcherWhat happens
BeforeToolwrite_file, replace, run_shell_commandWith the guard enabled, asks Ward about tool_input.file_path or tool_input.command and refuses protected changes with decision: "deny".
AfterToolwrite_file, replaceRuns the quality check. Findings are appended to the tool result as additionalContext, so the edit is still reported as applied.
AfterAgentRuns 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.

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:

Terminal window
junie --config-location .junie/config.json

Without this flag Junie runs no Ward hook, and Ward cannot report that it is missing.

Junie eventMatcherWhat happens
PreToolUseWrite, Edit, Bash, apply_patchWith 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.
StopRuns 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.

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 eventMatcherWhat happens
PreToolUseCreate, Edit, MultiEdit, ApplyPatch, ExecuteWith 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".
PostToolUseCreate, Edit, MultiEdit, ApplyPatchRuns the quality check. Failures exit 2, so Droid gives the findings to the model.
StopRuns 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.

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 eventMatcherWhat happens
preToolUseShell, Write, DeleteWith 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".
postToolUseWrite, DeleteRuns the quality check. Findings are returned as additional_context, which Cursor adds to the conversation.
stopRuns 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.

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 eventWhat happens
pre_write_codeWith 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_commandWith the guard enabled, asks Ward about tool_info.command_line in its cwd, and blocks protected commands the same way.
post_write_codeRuns 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 fileToolsWhat happens
PreToolUseeditor, apply_patch, run_commandsWith the guard enabled, asks Ward about the edited path, the patch, or each command, and stops protected changes with cancel: true and the reason.
PostToolUseeditor, apply_patchRuns 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 eventWhat the plugin does
tool.callFor 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.resultAfter 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.endWhen 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.