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:
| Event | When | What Ward does |
|---|---|---|
PostToolUse (Edit, Write, MultiEdit) | After each file edit | Runs a partial quality check, then validates the edited spec file |
Stop | When Claude finishes its turn | Runs 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, andstop_hook_active.
After each edit
Section titled “After each edit”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
PostToolUsecontext, and nothing else is added.
When the turn ends
Section titled “When the turn ends”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 asystemMessageinstead 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 missingopenspecexecutable, 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.
Approved-artifact guard
Section titled “Approved-artifact guard”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 to | Result |
|---|---|
Edit or create files under openspec/changes/<change>/ | Allowed |
| Read anything, including approved specs and archived changes | Allowed |
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 path | Denied |
Invoke an OpenSpec archive or sync skill, such as opsx:archive or openspec-sync-specs | Denied |
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.
Ownership and removal
Section titled “Ownership and removal”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.jsonalready 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
hooksstructure causes a conflict. Ward then leaves the settings and the other integration targets unchanged. - Removal:
ward integrate claude-code --removeremoves only Ward’s hook registrations and ownership marker, deletes its owned hook scripts, and keeps unrelated settings and hooks. The.claude/settings.jsonfile remains, even if it ends up empty. - Refresh: re-run
ward integrate claude-codeto refresh an outdated Ward-owned hook.
Projects in a subdirectory
Section titled “Projects in a subdirectory”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(andward-openspec-guard-apps-apiwith the guard), and change to the repository root before runningward hook claude-code -C 'apps/api', so the session’s directory does not matter; - the root
.claude/settings.jsonkeeps 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.
Verification
Section titled “Verification”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) andANTHROPIC_API_KEY. - Without them, every scenario is reported as blocked and the command exits non-zero.
- No live run has been recorded yet.