OpenCode
Install with ward integrate opencode, and make sure ward is on PATH. Ward keeps the
.opencode/commands/ward-check.md command and creates .opencode/plugins/ward-edit-write.js.
The plugin targets the OpenCode V2 plugin API (Plugin.define from @opencode/plugin, verified
against 2.0.30) and handles two moments:
| Hook | When | What Ward does |
|---|---|---|
ctx.tool.hook("execute.after") | After a completed edit, write, multiedit, apply_patch, or patch tool call | Runs a partial quality check, then validates the edited spec files |
ctx.event.subscribe() (session.idle) | When the agent finishes its turn | Runs every configured spec check |
After each edit
Section titled “After each edit”The plugin resolves the session’s current location (location.directory from ctx.session.get) and runs
ward check --format json --exclude-spec --learn-after 3 there for the edited paths. It collects them
from:
args.filePathforedit,write, andmultiedit;- the file and move paths in the
apply_patchresult metadata; args.pathfor the newerpatchtool.
Each path is passed to the check as --path, so it is a partial check: gates the
edit does not affect are skipped, and supported tools analyze only the edited files.
- A tool call that reports no path runs every gate in full.
- The idle hook validates specs only, so files changed by shell commands are not quality-checked until the Git hooks or CI run.
- Paths under
openspec/are passed toward spec validate. A single patch that touches several changes validates each of them once.
What the agent sees
Section titled “What the agent sees”If the quality check fails or spec validation reports findings, the plugin appends Ward’s output to the tool result the agent reads, so the agent sees it immediately. Learning prompts are appended the same way, whether the check passed or failed.
- The tool call itself still completes. OpenCode V2 does not let a post-tool hook fail a call, and a hook that throws would break the call instead of reporting the problem.
- Failed tool calls are left untouched and don’t trigger a check.
- If the session location can’t be resolved, the plugin skips validation with a bounded stderr diagnostic rather than guessing a directory.
When the session becomes idle
Section titled “When the session becomes idle”The plugin runs ward spec validate without paths, which runs the enabled openspec and
openspec-archived validations.
- No problems: nothing happens.
- First idle with findings in a turn: the plugin queues a prompt to the session with the findings,
prefixed with
[ward spec validation], so the agent can fix them. - Agent goes idle again and findings remain: the plugin emits a bounded stderr warning instead of prompting again. A new message from you allows one more hand-off. The plugin’s own prompt doesn’t count as a new user turn, so a problem the agent cannot fix can’t loop.
- Ward itself fails: the bounded stderr diagnostic is never sent to the agent.
- Overlapping idle events: if another idle event arrives while a validation is still running for the same session, it is ignored.
Projects without an enabled OpenSpec validation see no change: spec validation does nothing for them.
Approved-artifact guard
Section titled “Approved-artifact guard”The approved-artifact guard is opt-in. Install it with
ward integrate opencode --guard-approved-specs. Ward creates the separate,
Ward-owned .opencode/plugins/ward-openspec-guard.js plugin. It uses the V2
ctx.tool.hook("execute.before") registration and covers these tools:
| Tool | Values passed to ward spec guard |
|---|---|
edit, write | args.filePath (or args.path) |
apply_patch, patch | args.patchText (or args.patch) on stdin |
bash | args.command on stdin and args.workdir as --cwd |
skill | args.name (or args.skill) |
Calls that would modify openspec/specs/** or openspec/changes/archive/**, archive OpenSpec
changes, or invoke an archive or sync skill throw the refusal returned by Ward and are stopped.
Active changes remain editable. The guard is independent of the post-tool quality and validation
plugin, and it never replaces OpenCode’s own permissions.
| Situation | Result |
|---|---|
Ward cannot decide: missing from PATH, cannot start, exits 2, or the session location can’t be resolved | Bounded stderr warning; the tool call proceeds |
| Input Ward cannot inspect | Refusal (exit 1) |
Relative workdir | Resolved against the session location |
Refreshing ward integrate opencode keeps the current guard state. Use
--no-guard-approved-specs to remove only this plugin, or --remove to remove the complete
Ward-owned integration.
Projects in a subdirectory
Section titled “Projects in a subdirectory”OpenCode sessions in a monorepo usually start at the repository root. When ward init configures a
project below it, such as apps/api, it installs .opencode/commands/ward-check-apps-api.md and
.opencode/plugins/ward-edit-write-apps-api.js (plus ward-openspec-guard-apps-api.js with the
guard) at the repository root. These plugins:
- run
wardfrom the project directory, found from the plugin’s own location, so the session’s directory does not matter; - pass edited paths as absolute paths, resolved against the session directory;
- use plugin ids suffixed with the project (
ward.edit-write.apps-api), so plugins of several projects can load side by side.
An edit outside the project runs none of its gates.
Verification
Section titled “Verification”| Suite | What it proves |
|---|---|
bun run test in compatibility/opencode-v2 (deterministic) | The generated plugins load against @opencode/plugin 2.0.30 and behave as described above. It proves they match the package contract, not that a running OpenCode activates them. |
bun run smoke (opt-in live run) | Launches OpenCode 2.0.30 in a disposable fixture. Its activation check needs no model: on 2026-10-16, OpenCode 2.0.30 discovered and loaded both plugins without error. |
- The edit-feedback and guard scenarios need a model and a provider key, and have not been run yet.
- Idle-limit and reload behavior have no live driver.
- Missing prerequisites are reported as blocked, not as a pass. Installed files alone do not prove runtime activation.
Ownership and removal
Section titled “Ownership and removal”- Generated files contain ownership markers. User-owned targets are never overwritten, and a conflict does not enable a partial integration.
- Remove with
ward integrate opencode --remove. Only Ward-owned generated files are deleted. - Re-run
ward integrate opencodeto refresh an outdated Ward-owned plugin. Earlier plugins did not recognizeapply_patch, so patch edits went unchecked until refreshed. - Hooks execute trusted repository configuration with your privileges.