Skip to content

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:

HookWhenWhat Ward does
ctx.tool.hook("execute.after")After a completed edit, write, multiedit, apply_patch, or patch tool callRuns a partial quality check, then validates the edited spec files
ctx.event.subscribe() (session.idle)When the agent finishes its turnRuns every configured spec check

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.filePath for edit, write, and multiedit;
  • the file and move paths in the apply_patch result metadata;
  • args.path for the newer patch tool.

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 to ward spec validate. A single patch that touches several changes validates each of them once.

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.

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.

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:

ToolValues passed to ward spec guard
edit, writeargs.filePath (or args.path)
apply_patch, patchargs.patchText (or args.patch) on stdin
bashargs.command on stdin and args.workdir as --cwd
skillargs.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.

SituationResult
Ward cannot decide: missing from PATH, cannot start, exits 2, or the session location can’t be resolvedBounded stderr warning; the tool call proceeds
Input Ward cannot inspectRefusal (exit 1)
Relative workdirResolved 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.

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 ward from 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.

SuiteWhat 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.
  • 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 opencode to refresh an outdated Ward-owned plugin. Earlier plugins did not recognize apply_patch, so patch edits went unchecked until refreshed.
  • Hooks execute trusted repository configuration with your privileges.