feat(opencode)!: run the hooks as OpenCode 2 plugins #168

Merged
claudiomaradonna merged 1 commit from feat/opencode-v2-plugins into main 2026-10-29 14:10:59 +01:00
Collaborator

Why

On OpenCode 2, an agent writes a shell script and no shell check
follows. It can also edit a tracked file in the main checkout on its
main branch, and nothing refuses it. OpenCode 2 runs no plugin in
OpenCode 1's shape: it reads only a module's default export, and logs
both of our plugins as failed to load. Everyone who moves to OpenCode 2
loses both hooks without noticing. Fixes #134.

Two alternatives were set aside. A dual export in each file would keep
1.x support, but it rests on a form I could not verify here. Separate
V2 files would leave V2 logging the V1 files as failed. You chose to
support V2 only.

What changed

Breaking change: the OpenCode hooks now run on OpenCode 2.x only. A
repository still on 1.x must upgrade OpenCode to keep them.

Both plugins default-export an OpenCode 2 definition:

  • The shell check runs after a completed write, edit or patch and
    appends its report to the tool's output, which the agent reads in the
    same turn.
  • The guard runs before each of those calls and refuses it with the
    guard's reason.

A shared module, .opencode/lib/hook-event.js, hands the unchanged
shared hooks the event in the form they already read: apply_patch
for patch and filePath for path. It also gives them each path as
OpenCode 2 resolves it: ~ becomes the home directory, and patch lines
and header paths are trimmed. Without this, the security review showed
that ~/… paths, indented patch headers or extra spaces before a path
let an edit of the main checkout through.

The plugins import nothing from @opencode/plugin, so the change adds
no dependency. The validator pins the V2 lines and requires the module.
The README says the hooks were written against 2.1.4's plugin API and
have not yet run in OpenCode, and lists Node.js as needed to maintain
the source.

How to verify

You need Node.js and jq. From a fresh checkout of the branch, run
bash tests/agent-config/opencode-plugins.sh. It loads each plugin the
way OpenCode 2 does, from its default export. It uses stub hooks that
record the event they get, and expects 14 passing cases, among them:

  • an edit's path arrives as filePath, and a patch as
    apply_patch with its patchText;
  • ~/repo/f.sh arrives as /home/user/repo/f.sh;
  • indented headers, a doubled space before a ~ path and a path
    holding a carriage return arrive as OpenCode reads them;
  • the guard's refusal rejects the call with its reason;
  • the shell check's report is appended to the tool's content, and is
    skipped after a failed call.

On main the area fails at its first case, because the plugins have
no default export.

To see the real thing, put the branch's .opencode/ in a repository,
start OpenCode 2 there in the main checkout on main, and ask it to
edit a tracked file. The edit should be refused with "main-checkout
guard: … stays as it was".

The README's Maintenance checks pass on 941ccce, with 315
agent-configuration tests and 55 shell files clean. Four rounds of
change and security review closed every finding. I could not run the
plugins inside an OpenCode session, because it needs a model provider.
Their behavior there rests on the 2.1.4 package's types and bundled
runtime, which I read but did not run. That package was installed in
the session scratchpad with --ignore-scripts. The behavioral evals
did not run either.

Considerations

The README's Maintenance prettier --check still covers Markdown only.
The plugins' JavaScript passes Prettier, but nothing enforces it there;
that is the same open decision as on #164.

## Why On OpenCode 2, an agent writes a shell script and no shell check follows. It can also edit a tracked file in the main checkout on its main branch, and nothing refuses it. OpenCode 2 runs no plugin in OpenCode 1's shape: it reads only a module's default export, and logs both of our plugins as failed to load. Everyone who moves to OpenCode 2 loses both hooks without noticing. Fixes #134. Two alternatives were set aside. A dual export in each file would keep 1.x support, but it rests on a form I could not verify here. Separate V2 files would leave V2 logging the V1 files as failed. You chose to support V2 only. ## What changed **Breaking change:** the OpenCode hooks now run on OpenCode 2.x only. A repository still on 1.x must upgrade OpenCode to keep them. Both plugins default-export an OpenCode 2 definition: - The shell check runs after a completed `write`, `edit` or `patch` and appends its report to the tool's output, which the agent reads in the same turn. - The guard runs before each of those calls and refuses it with the guard's reason. A shared module, `.opencode/lib/hook-event.js`, hands the unchanged shared hooks the event in the form they already read: `apply_patch` for `patch` and `filePath` for `path`. It also gives them each path as OpenCode 2 resolves it: `~` becomes the home directory, and patch lines and header paths are trimmed. Without this, the security review showed that `~/…` paths, indented patch headers or extra spaces before a path let an edit of the main checkout through. The plugins import nothing from `@opencode/plugin`, so the change adds no dependency. The validator pins the V2 lines and requires the module. The README says the hooks were written against 2.1.4's plugin API and have not yet run in OpenCode, and lists Node.js as needed to maintain the source. ## How to verify You need Node.js and `jq`. From a fresh checkout of the branch, run `bash tests/agent-config/opencode-plugins.sh`. It loads each plugin the way OpenCode 2 does, from its default export. It uses stub hooks that record the event they get, and expects 14 passing cases, among them: - an edit's `path` arrives as `filePath`, and a `patch` as `apply_patch` with its `patchText`; - `~/repo/f.sh` arrives as `/home/user/repo/f.sh`; - indented headers, a doubled space before a `~` path and a path holding a carriage return arrive as OpenCode reads them; - the guard's refusal rejects the call with its reason; - the shell check's report is appended to the tool's content, and is skipped after a failed call. On `main` the area fails at its first case, because the plugins have no default export. To see the real thing, put the branch's `.opencode/` in a repository, start OpenCode 2 there in the main checkout on `main`, and ask it to edit a tracked file. The edit should be refused with "main-checkout guard: … stays as it was". The README's Maintenance checks pass on 941ccce, with 315 agent-configuration tests and 55 shell files clean. Four rounds of change and security review closed every finding. I could not run the plugins inside an OpenCode session, because it needs a model provider. Their behavior there rests on the 2.1.4 package's types and bundled runtime, which I read but did not run. That package was installed in the session scratchpad with `--ignore-scripts`. The behavioral evals did not run either. ## Considerations The README's Maintenance `prettier --check` still covers Markdown only. The plugins' JavaScript passes Prettier, but nothing enforces it there; that is the same open decision as on #164.
OpenCode 2 runs no plugin in OpenCode 1's shape, so on it no shell
check followed a write and nothing refused an edit in the main
checkout. Both plugins now default-export a V2 definition that hooks
execute.after and execute.before. A shared module gives the hooks the
event under the names they read, apply_patch for patch and filePath for
path, with each path as OpenCode 2 resolves it: ~ as the home directory
and patch lines and header paths trimmed. A new test area runs both
plugins under node with stub hooks; the validator pins the V2 lines and
requires the module.

BREAKING CHANGE: the OpenCode hooks run on OpenCode 2.x only; a
repository still on 1.x upgrades OpenCode to keep them.

Fixes #134
motoko force-pushed feat/opencode-v2-plugins from 941cccef49 to 0fa031286f 2026-10-29 14:09:51 +01:00 Compare
claudiomaradonna deleted branch feat/opencode-v2-plugins 2026-10-29 14:11:04 +01:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
claudiomaradonna/agent-foundry!168
No description provided.