Skip to content

Claude Code

Install with ward integrate claude-code. Ward creates or updates .claude/settings.json and the executable .claude/hooks/ward-edit-write, and registers the same hook for two Claude Code events:

EventWhenWhat Ward does
PostToolUse (Edit, Write, MultiEdit)After each file editRuns a partial quality check, then validates the edited spec file
StopWhen Claude finishes its turnRuns the full default quality check plus every configured spec check

Make sure ward is on PATH. The generated hook runs ward hook claude-code --learn-after 3 --exclude-spec --event-stdin for the project directory.

  • Claude Code runs hooks in the session’s current directory, so the registrations point at ${CLAUDE_PROJECT_DIR}/.claude/hooks/... and the hook passes -C "$CLAUDE_PROJECT_DIR" to Ward. Checks still run for the project after the agent changes into a subdirectory.
  • Claude Code passes each hook event as JSON on stdin. Ward reads only the event name, the edited file_path, and stop_hook_active.

Ward runs a partial check of the default profile for the edited file, skipping spec gates (--exclude-spec). Gates with a partial table are skipped when the file is not relevant to them and narrowed to it when they can be; other gates run in full. It then runs targeted spec validation for the edited file. An edit outside openspec/changes/<change>/ or openspec/specs/ validates nothing extra.

  • Something fails: a quality gate failure, spec findings, or both are written to stderr with exit code 2. Claude Code shows them to the agent, which can fix them right away. Learning prompts are included too.
  • Everything passes: learning prompts, if any, are returned as PostToolUse context, and nothing else is added.

On Stop, Ward runs the full default profile, without spec gates, and the enabled openspec and openspec-archived validations. Files changed by shell commands send no edit event, so this full check is where they are caught.

  • No problems: the turn ends normally with no output.
  • First stop with findings: Ward blocks the stop with {"decision": "block", "reason": ...}, so Claude receives the findings and keeps working on them.
  • Claude stops again and findings remain: Claude Code marks this with stop_hook_active. Ward then lets the turn end and shows the findings to you as a systemMessage instead of sending them to the agent again. Findings are handed to the agent at most once per turn, so a problem the agent cannot fix, such as a missing openspec executable, cannot trap it in a loop.
  • Ward itself fails, for example on an invalid ward.toml: the error is shown to you and the stop is never blocked.

Projects without an enabled OpenSpec validation see no change: per-edit spec validation and the Stop hook do nothing.

The guard is opt-in. Install it with ward integrate claude-code --guard-approved-specs. Ward then creates the executable .claude/hooks/ward-openspec-guard and registers it in .claude/settings.json as a PreToolUse hook for ^(Edit|Write|MultiEdit|NotebookEdit|Bash|Skill)$. Before each of those tool calls, it runs ward hook claude-code --guard-approved-specs --event-stdin.

The agent tries toResult
Edit or create files under openspec/changes/<change>/Allowed
Read anything, including approved specs and archived changesAllowed
Edit, Write, MultiEdit, or NotebookEdit a file under openspec/specs/ or openspec/changes/archive/Denied
Run a Bash command recognized as openspec archive, or one that writes, deletes, or moves a protected pathDenied
Invoke an OpenSpec archive or sync skill, such as opsx:archive or openspec-sync-specsDenied

A denied call returns permissionDecision: "deny" with a reason that names the blocked path. For an approved spec, the reason gives the delta path to edit instead (openspec/changes/<change>/specs/<capability>/spec.md) and lists the active changes. Claude receives the reason and can redirect its work.

Archiving and syncing stay with you. With the guard installed, Claude cannot complete /opsx:archive or /opsx:sync on its own. Run the command yourself when the change is approved, for example by typing ! openspec archive <change> in Claude Code, which runs it directly in your shell.

Failures do not block your work. If ward is not on PATH or Ward cannot decide, the hook reports the problem on stderr with a non-blocking exit code and the tool call goes ahead. A payload Ward cannot read is denied, because the guard cannot see what the call would change.

Know the limits. Bash filtering is a heuristic, not a security boundary. Scripts, variables, and other indirect writes are not prevented. See spec guard and security.

The guard is independent of the edit and turn-end hooks. It runs whether or not OpenSpec validation is enabled in ward.toml, and installing or removing it does not change them. Re-running ward integrate claude-code keeps the guard as it is. --no-guard-approved-specs removes only the guard, and --remove removes everything.

Generated files contain Ward ownership markers. Existing user-owned targets are never overwritten, and a conflict prevents a partial integration.

  • Existing settings: if .claude/settings.json already contains valid JSON, Ward merges its hook registrations into it and keeps unrelated settings and hooks. It never replaces the file.
  • Conflicts: malformed JSON, a non-object settings root, or an incompatible hooks structure causes a conflict. Ward then leaves the settings and the other integration targets unchanged.
  • Removal: ward integrate claude-code --remove removes only Ward’s hook registrations and ownership marker, deletes its owned hook scripts, and keeps unrelated settings and hooks. The .claude/settings.json file remains, even if it ends up empty.
  • Refresh: re-run ward integrate claude-code to refresh an outdated Ward-owned hook.

Claude Code loads .claude/settings.json from the directory a session starts in, which in a monorepo is usually the repository root. When ward init configures a project below the root, such as apps/api, it therefore installs the integration at the repository root:

  • the hook scripts are named after the project, .claude/hooks/ward-edit-write-apps-api (and ward-openspec-guard-apps-api with the guard), and change to the repository root before running ward hook claude-code -C 'apps/api', so the session’s directory does not matter;
  • the root .claude/settings.json keeps every hook you already registered. Ward adds or replaces only the entries of the project being installed, so several Ward projects can share the file;
  • an edit outside the project runs none of its gates, and at the end of a turn the full check runs only when a file under the project differs from HEAD (git status -- . in the project).

Removing one project’s integration leaves the other projects’ hooks and your own in place; the wardOwnership key goes away with the last Ward entry.

The deterministic suite in compatibility/claude-code (bun run test) runs the generated settings and hook scripts against the real Ward, the way Claude Code launches them: from a subdirectory, with CLAUDE_PROJECT_DIR set. It covers:

  • edit checks and their exit-2 feedback, and learning context;
  • the guard’s deny decision and uninspectable input;
  • the one-time stop block;
  • settings merge and removal.

It proves the generated files match Claude Code’s hook contract, not that a running Claude Code loads them.

The opt-in live run, bun run src/cli.ts live claude-code from compatibility/, runs headless claude -p turns against Claude Code 2.1.292 in a disposable fixture. An isolated CLAUDE_CONFIG_DIR keeps user and global settings out.

  • It needs WARD_COMPAT_CLAUDE_CODE_BIN (an absolute path to that release) and ANTHROPIC_API_KEY.
  • Without them, every scenario is reported as blocked and the command exits non-zero.
  • No live run has been recorded yet.