spec
ward spec validate [-C <dir>] [--profile <name>[,<name>...]] [--] [<path>...]ward spec guard [-C <dir>] [--cwd <dir>] [--patch-stdin | --shell-stdin | --skill <name>] [--] [<path>...]ward spec enforce [-C <dir>] --base <revision> [--profile <name>[,<name>...]] [--bootstrap-baseline <declaration>]ward spec validate checks a project’s specification artifacts and prints findings that a person or a
coding agent can act on. Any agent integration can call it with the files it just edited. The generated
Claude Code and OpenCode integrations do this for you.
Today it supports OpenSpec. It is a no-op unless an OpenSpec validation is enabled in
ward.toml, so projects without OpenSpec see no change.
Options
Section titled “Options”| Option | Subcommand | What it does |
|---|---|---|
-C, --directory <dir> | all | Run for the project in <dir> instead of the current directory. |
--profile <name>[,...] | validate, enforce | Profiles whose OpenSpec registrations run (default: default). |
<path>... | validate, guard | Files to validate, or files the agent would write. |
--cwd <dir> | guard | Directory that relative paths and shell commands resolve against (default: the project root). |
--patch-stdin | guard | Read a patch from stdin. |
--shell-stdin | guard | Read a shell command from stdin. |
--skill <name> | guard | Check a skill or command invocation. |
--base <revision> | enforce | The base revision to compare with. Required. |
--bootstrap-baseline <declaration> | enforce | Accept the first baseline explicitly. See spec enforce. |
--verbose | all | Print diagnostics to stderr. |
guard takes exactly one kind of input: paths, --patch-stdin, --shell-stdin, or --skill.
Targeted validation: with paths
Section titled “Targeted validation: with paths”Pass the files that changed. Paths may be absolute or relative to the project root.
ward spec validate openspec/changes/add-auth/proposal.md openspec/specs/identity/user-auth/spec.mdWard maps each path to the active item it belongs to and validates every distinct item once:
| Path | Validated item |
|---|---|
openspec/changes/<change>/** | Change <change> |
openspec/specs/<capability>/spec.md | Spec <capability>, including nested paths such as identity/user-auth |
openspec/changes/archive/** | Nothing; archived changes are not active |
| Anything else | Nothing |
For each item Ward runs the executable configured in the openspec registration, from the project root,
as a direct argument vector:
<openspec executable> validate <item> --type <change|spec> --strict --no-interactive- Targeted validation runs only when the active
openspecregistration is enabled in the selected profiles. Enabling onlyopenspec-archiveddoes not turn it on. - Paths that leave the project root are ignored, whether through
..or a symbolic link. When theopenspecregistration setsroot, asward initdoes for a project in a monorepo whoseopenspec/lives at the repository root, paths are contained in that root instead and the executable runs from it; arootoutside the repository validates nothing. - Items that no longer exist are skipped silently, for example after a change was deleted or archived during the edit.
- A missing executable or project-root
openspec/directory is reported as a finding, and nothing is launched.
Full validation: without paths
Section titled “Full validation: without paths”ward spec validate --profile ciWithout paths, Ward runs the enabled openspec and openspec-archived registrations for the selected
profiles (default: default). It uses the configured commands, prerequisites, and statuses, exactly as
check would. Code-quality gates do not run. If neither registration is enabled, or the
project has no ward.toml, there is nothing to do and the command exits 0.
Output
Section titled “Output”Findings are read from both stdout and stderr, because OpenSpec writes validation problems to stderr. They are
printed on stdout grouped by item, with the most severe first. Validator hints such as Next steps: are left out.
openspec change bad: 1 error, 1 warning ERROR x/spec.md: ADDED "Thing" must include at least one scenario WARNING x/spec.md: ADDED "Thing" should contain SHALL or MUSTOutput is bounded so it fits agent hook context limits:
- At most 20 findings are printed per item, followed by
... N more findings omittedwhen there are more. - The whole output is capped at about 8,000 characters.
- When a validation fails without recognizable
ERROR,WARNING, orINFOlines, a short excerpt of its output is shown instead.
Validate exit codes
Section titled “Validate exit codes”| Code | Meaning |
|---|---|
0 | Nothing to report: everything validated, or there was nothing to validate. stdout is empty. |
1 | Findings were printed, or an enabled validation was unsuccessful (including UNAVAILABLE). |
2 | Ward could not run: no project, invalid configuration, or an unknown profile. The message goes to stderr. |
With --verbose, diagnostics on stderr report item counts, kinds, statuses, and durations. They never include
findings, artifact contents, or command lines.
Guarding approved artifacts: spec guard
Section titled “Guarding approved artifacts: spec guard”ward spec guard decides, before an agent tool call runs, whether that call would modify approved OpenSpec
artifacts. The opt-in Claude Code and OpenCode guards call
it for you; any other agent integration can call it too.
| Path | Agents may |
|---|---|
openspec/changes/<change>/** | Create, edit, delete, and move freely |
openspec/specs/** | Read only |
openspec/changes/archive/** | Read only |
openspec/, openspec/changes/, the project root | Not delete or move, because that would remove the protected directories |
Archiving a change and syncing deltas into openspec/specs/ require a human. Run openspec archive <change>
yourself.
Pass exactly one kind of input:
# Files the agent would write, absolute or relative to --cwd (default: the project root)ward spec guard -- openspec/changes/add-auth/design.md
# A patch, in the *** Begin Patch format used by OpenCode and Codex or as a unified/git diffward spec guard --patch-stdin < change.patch
# A shell command, resolved against the directory it runs inprintf '%s' 'rm -rf openspec/specs/auth' | ward spec guard --shell-stdin --cwd .
# A skill or command invocationward spec guard --skill opsx:archiveWhat the guard checks:
- Every patch target. Added, updated, deleted, and moved files are all inspected, not just the first one. A move is refused when either its source or its destination is protected.
- Path aliases. Paths are normalized for
.and.., and protection is decided by file identity. A symbolic link toopenspec/specs, a protected directory that is itself a link elsewhere, and case variants such asOpenSpec/Specson case-insensitive file systems are all treated as the protected directory. - Shell commands (heuristic). Recognized
openspec archiveinvocations are refused, includingnpx,pnpm exec, path-qualified, and chained forms. Output redirections into protected paths are refused. Any other command that names a protected path is refused unless it is a known read-only or validation command, such ascat,grep,ls,git diff,openspec validate|show|list|status, orward spec validate. Simple patterns such asrm -rf openspec/*are matched against the protected directories. - Skills. OpenSpec archive and sync skills such as
opsx:archive,opsx:sync,openspec-archive-change, andopenspec-sync-specsare refused. Other skills are allowed. - Input it cannot interpret is refused. This covers a patch with no recognizable file header, a shell command with an unterminated quote, and input larger than 16 MiB.
The guard reads no ward.toml gate and launches no process. In a project without an openspec/ directory,
everything is allowed.
A refusal tells the agent where to work instead:
Ward blocked this tool call: approved OpenSpec artifacts are read-only for agents.- openspec/specs/identity/user-auth/spec.md is an approved spec. Record the change as a delta in an active change: openspec/changes/<change>/specs/identity/user-auth/spec.mdActive changes: add-auth, fix-loginArchiving a change or syncing deltas into openspec/specs/ requires a human. Ask the user to run `openspec archive <change>` (or the sync workflow) themselves.When no change is active, the reason explains how to create one with openspec new change <name>. The reason is
capped at 4,000 characters.
The guard never reads ward.toml. It protects the project’s own openspec/; a project below the Git
top-level that has none protects the top-level’s openspec/ instead, the same directory ward init
points the OpenSpec gates at. Relative paths still start from --cwd or the project root.
Guard exit codes
Section titled “Guard exit codes”| Code | Meaning |
|---|---|
0 | Allowed. stdout is empty. |
1 | Refused. The reason is printed on stdout. |
2 | Ward could not decide, for example outside a detectable project or with conflicting options. The message goes to stderr. |
With --verbose, diagnostics on stderr report the input kind, target count, refusal reasons, and the decision. They
never include paths, patch text, or command text.
Enforcing revision provenance: spec enforce
Section titled “Enforcing revision provenance: spec enforce”Use the blocking command from a repository checkout:
ward spec enforce --base origin/mainWard resolves the checked-out HEAD, computes git merge-base with the explicit base, and compares the
merge-base OpenSpec snapshot with the candidate. The checkout must contain the base revision and enough history
for that merge base. Shallow or missing history fails with exit code 2; repair it with
actions/checkout fetch-depth: 0 (or an equivalent fetch).
- The command reuses the configured active, archived, and optional JUnit traceability checks.
- Existing archives are immutable.
- A changed live specification must be produced exactly by one newly added, complete archive. An archive name or commit message alone is not enough.
For the first baseline only, a reviewer may explicitly select bootstrap:
ward spec enforce --base origin/main --bootstrap-baseline "approved initial baseline"Missing history never implicitly enables bootstrap. Git history and archive provenance are evidence of deterministic
origin, not proof of human approval; required status checks and code-owner review remain repository settings.
Generated blocking integrations never invoke /opsx:verify, which remains advisory.
Exit codes
Section titled “Exit codes”| Subcommand | 0 | 1 | 2 |
|---|---|---|---|
spec validate | Nothing to report | Findings, or an unsuccessful validation | Ward could not run |
spec guard | Allowed | Refused; the reason is on stdout | Ward could not decide |
spec enforce | The checks pass | Policy findings | Ward could not run, including shallow or missing history |
See Validate exit codes and Guard exit codes for details.