Skip to content

spec

Terminal window
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.

OptionSubcommandWhat it does
-C, --directory <dir>allRun for the project in <dir> instead of the current directory.
--profile <name>[,...]validate, enforceProfiles whose OpenSpec registrations run (default: default).
<path>...validate, guardFiles to validate, or files the agent would write.
--cwd <dir>guardDirectory that relative paths and shell commands resolve against (default: the project root).
--patch-stdinguardRead a patch from stdin.
--shell-stdinguardRead a shell command from stdin.
--skill <name>guardCheck a skill or command invocation.
--base <revision>enforceThe base revision to compare with. Required.
--bootstrap-baseline <declaration>enforceAccept the first baseline explicitly. See spec enforce.
--verboseallPrint diagnostics to stderr.

guard takes exactly one kind of input: paths, --patch-stdin, --shell-stdin, or --skill.

Pass the files that changed. Paths may be absolute or relative to the project root.

Terminal window
ward spec validate openspec/changes/add-auth/proposal.md openspec/specs/identity/user-auth/spec.md

Ward maps each path to the active item it belongs to and validates every distinct item once:

PathValidated item
openspec/changes/<change>/**Change <change>
openspec/specs/<capability>/spec.mdSpec <capability>, including nested paths such as identity/user-auth
openspec/changes/archive/**Nothing; archived changes are not active
Anything elseNothing

For each item Ward runs the executable configured in the openspec registration, from the project root, as a direct argument vector:

Terminal window
<openspec executable> validate <item> --type <change|spec> --strict --no-interactive
  • Targeted validation runs only when the active openspec registration is enabled in the selected profiles. Enabling only openspec-archived does not turn it on.
  • Paths that leave the project root are ignored, whether through .. or a symbolic link. When the openspec registration sets root, as ward init does for a project in a monorepo whose openspec/ lives at the repository root, paths are contained in that root instead and the executable runs from it; a root outside 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.
Terminal window
ward spec validate --profile ci

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

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 MUST

Output is bounded so it fits agent hook context limits:

  • At most 20 findings are printed per item, followed by ... N more findings omitted when there are more.
  • The whole output is capped at about 8,000 characters.
  • When a validation fails without recognizable ERROR, WARNING, or INFO lines, a short excerpt of its output is shown instead.
CodeMeaning
0Nothing to report: everything validated, or there was nothing to validate. stdout is empty.
1Findings were printed, or an enabled validation was unsuccessful (including UNAVAILABLE).
2Ward 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.

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.

PathAgents may
openspec/changes/<change>/**Create, edit, delete, and move freely
openspec/specs/**Read only
openspec/changes/archive/**Read only
openspec/, openspec/changes/, the project rootNot 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:

Terminal window
# 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 diff
ward spec guard --patch-stdin < change.patch
# A shell command, resolved against the directory it runs in
printf '%s' 'rm -rf openspec/specs/auth' | ward spec guard --shell-stdin --cwd .
# A skill or command invocation
ward spec guard --skill opsx:archive

What 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 to openspec/specs, a protected directory that is itself a link elsewhere, and case variants such as OpenSpec/Specs on case-insensitive file systems are all treated as the protected directory.
  • Shell commands (heuristic). Recognized openspec archive invocations are refused, including npx, 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 as cat, grep, ls, git diff, openspec validate|show|list|status, or ward spec validate. Simple patterns such as rm -rf openspec/* are matched against the protected directories.
  • Skills. OpenSpec archive and sync skills such as opsx:archive, opsx:sync, openspec-archive-change, and openspec-sync-specs are 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.md
Active changes: add-auth, fix-login
Archiving 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.

CodeMeaning
0Allowed. stdout is empty.
1Refused. The reason is printed on stdout.
2Ward 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:

Terminal window
ward spec enforce --base origin/main

Ward 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:

Terminal window
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.

Subcommand012
spec validateNothing to reportFindings, or an unsuccessful validationWard could not run
spec guardAllowedRefused; the reason is on stdoutWard could not decide
spec enforceThe checks passPolicy findingsWard could not run, including shallow or missing history

See Validate exit codes and Guard exit codes for details.