Skip to content

Add Cursor and OpenCode adapters - #25

Merged
spaceamoeba-t merged 9 commits into
mainfrom
claude/cursor-opencode-adapters
Aug 12, 2026
Merged

spaceamoeba-t merged 9 commits into
mainfrom
claude/cursor-opencode-adapters

Conversation

@spaceamoeba-t

Copy link
Copy Markdown
Owner

Two new agent adapters connect Cursor and OpenCode to the existing agent-neutral broker, following the deliberately narrow slice the Codex adapter established: native approval prompts, completion announcements, and fail-open on every error path so the agent's own on-screen flow always remains in control.

Cursor (TapQCursorAdapter, tapq-cursor-hook)

  • tapq integration cursor install|status|uninstall manages TapQ-owned entries in ~/.cursor/hooks.json, preserving unrelated hook data and snapshotting an existing file to a restrictive backup before changing it.
  • Answers beforeShellExecution for non-sandboxed commands and preToolUse for the Write and Delete file tools; stop announces completion. Cursor exposes no hookable question tool and no final assistant text on stop, so question routing is absent by design.
  • Payload shapes follow Cursor's published hook reference (https://cursor.com/docs/agent/hooks); there is no recorded fixture corpus yet — docs/CURSOR_ADAPTER_MANUAL_TEST_PLAN.md pins what to confirm against a real client (tool_input keys, agent_message casing, sandbox flag).

OpenCode (TapQOpenCodeAdapter, tapq-opencode-hook)

  • OpenCode has no hook-registration file, so tapq integration opencode install writes one dependency-free plugin TapQ owns end to end at <config>/plugins/tapq.js; install refuses to overwrite a file TapQ did not write, and uninstall removes only TapQ's own plugin.
  • The plugin observes the permission.asked bus event and applies the hands-free answer through OpenCode's permission API — allow becomes a one-time once, deny becomes reject, and the remembered always reply is never sent. The documented permission.ask plugin hook is deliberately unused: it is declared in @opencode-ai/plugin but never triggered by OpenCode (permission.ask plugin hook is defined but not triggered anomalyco/opencode#7006).
  • Verified against OpenCode 1.18.15 source; process-contract tests drive the real hook executable against a real broker, and docs/OPENCODE_ADAPTER_MANUAL_TEST_PLAN.md covers the boundaries only a live OpenCode can prove.

Verification

  • swift build clean; swift test: 1563 tests, 0 failures (baseline 1455 on main).
  • scripts/check-public-boundary.sh passes; scripts/package-runtime-app.sh builds and signs both new hook binaries into TapQRuntime.app.
  • Neither adapter has run inside a real Cursor or OpenCode session yet; both fail open, so a wrong payload assumption degrades to the agent's normal on-screen prompt.

The two adapters were developed on independent branches (claude/cursor-adapter, claude/opencode-adapter) and merged here with conflicts resolved; per-adapter history is preserved in the merge.

🤖 Generated with Claude Code

spaceamoeba-t and others added 9 commits August 9, 2026 01:14
Cursor's agent hooks are a flat `hooks.json` of per-event command entries with a
`matcher` field on the entry itself, and its permission-bearing hooks answer with a
top-level `permission` of allow/deny/ask. That is close enough to the Codex adapter's
config-file shape to reuse its installer idioms, but the event set and response schema
are Cursor's own, so the parsing lives in a new module.

The supported slice stays deliberately narrow: `beforeShellExecution` for commands
Cursor does not sandbox, `preToolUse` matching `Write` and `Delete` for the mutating
file tools, and `stop` for completion. Cursor runs the shell hook for every command
rather than only when it would prompt, so this is a strict pre-tool gate with no native
policy; sandboxed executions are skipped because Cursor never prompts for those.

Cursor documents `preToolUse` `tool_input` as an open object, which for a write can hold
the whole proposed file body, so the summary names the action from the tool type and
speaks only a path it can resolve — never the argument object. `stop` carries no final
assistant text, so completion is announced and no turn is ever continued. Every parse,
transport, and broker failure emits no hook output, which is also Cursor's documented
default for a failed hook.

Wire format from https://cursor.com/docs/agent/hooks; fixtures in the tests are written
from that reference rather than recorded from a pinned client release.

Co-Authored-By: Claude Fable 5 <[email protected]>
Matches the existing integration UX: TapQ-managed entries only, unrelated hook data
preserved, a restrictive backup before an existing file changes, and repair on rerun.

Cursor has no hook-trust step and no local command that reports hook activation, so
status validates TapQ's file layout and then states the documented client coverage
instead of executing a Cursor binary. That coverage matters: the desktop app fires every
installed hook, while `cursor-agent` does not fire `preToolUse`, leaving writes and
deletes native there.

Co-Authored-By: Claude Fable 5 <[email protected]>
Adds the integration-guide and CLI-reference sections plus a manual test plan modeled on
the Codex one. The plan carries extra weight here: this adapter ships no versioned Cursor
fixture corpus, so it is the only place the real payload shape is confirmed against a
live client, and several cases ask the tester to record observed field names.

Co-Authored-By: Claude Fable 5 <[email protected]>
OpenCode has no hook-registration file comparable to Claude Code's
settings.json or Codex's hooks.json. Its documented extension surface is a
JavaScript plugin loaded from the OpenCode configuration directory at
startup, so the unit of installation is one file TapQ owns end to end.

TapQOpenCodeAdapter therefore generates and installs that plugin, and
tapq-opencode-hook is the executable edge it relays to. The plugin observes
OpenCode's permission.asked and session-idle bus events, pipes a small TapQ
relay envelope to the hook, and applies the returned decision through
OpenCode's own permission API. All policy, broker authentication, and speech
rendering stay in Swift, so the JavaScript layer carries no security-relevant
logic and a bus-event rename is a one-line plugin edit.

The plugin's permission.ask hook is deliberately unused: it is declared in
@opencode-ai/plugin's types but no OpenCode code path triggers it
(anomalyco/opencode#7006), so a plugin built on it would silently never run.
Replies prefer POST /permission/{requestID}/reply and fall back to the
deprecated session-scoped route on the injected SDK client.

The plugin is embedded as source text rather than an SPM resource because
package-runtime-app.sh assembles TapQRuntime.app from executables alone, so a
resource bundle would be absent exactly where the installer normally runs.
Embedding also makes install, repair detection, and idempotence a plain
string comparison.

The supported slice stays narrow. Allow becomes a one-time `once` reply and
deny becomes `reject`; the remembered `always` reply is never sent, because a
hands-free answer is scoped to the prompt it was spoken for. bash, edit, and
webfetch get kind-specific speech from documented scalar metadata, with a URL
contributing only its host, and every other kind is spoken from its name
alone, so a permission's open-ended metadata object never reaches speech.
There is no question interception or final-response continuation, because
OpenCode exposes no equivalent surface.

Every parse, transport, and broker failure applies no reply at all, leaving
OpenCode's own prompt pending and answerable on screen.

Co-Authored-By: Claude Fable 5 <[email protected]>
The command matches the existing integration UX: TapQ-managed content only,
a restrictive timestamped backup before changing an existing file, atomic
replacement, and repair-on-rerun for a plugin left stale by a moved checkout
or a hand edit. A plugin whose contents already match is a byte-for-byte
no-op that creates no backup.

Because TapQ owns the whole file here rather than merging entries into a
shared one, ownership is explicit: only a file carrying TapQ's marker is
rewritten or removed. Install refuses to overwrite anything else at the
managed path and reports it instead, and uninstall leaves such a file alone,
so neighbouring plugins in the same directory are never touched.

The default path resolves the OpenCode configuration directory the way
OpenCode does — OPENCODE_CONFIG_DIR, then XDG_CONFIG_HOME/opencode, then
~/.config/opencode — and ignores a non-absolute value in either variable so
the plugin cannot land somewhere OpenCode will not scan.

Co-Authored-By: Claude Fable 5 <[email protected]>
package-runtime-app.sh now builds, copies, and signs tapq-opencode-hook
alongside the other adapters, so the installer running from TapQRuntime.app
can point at a hook that exists beside it. check-public-boundary.sh adds the
new module to the portable target list.

Co-Authored-By: Claude Fable 5 <[email protected]>
Adds an OpenCode section to the integration guide and the CLI reference, a
manual test plan modeled on the Codex one, and the roadmap, README, and
changelog updates.

The docs are explicit about what the automated coverage does and does not
claim: the process contracts drive the real hook against a real broker, but
they do not start OpenCode, load the plugin into its runtime, or prove that
OpenCode accepted the reply the plugin issues. The manual plan owns those
boundaries, along with the once-per-turn completion announcement, the speech
boundary for permission metadata, and answering on screen while TapQ is still
speaking.

Co-Authored-By: Claude Fable 5 <[email protected]>
Resolves the adjacent-entry conflicts between claude/cursor-adapter and
claude/opencode-adapter: both adapter products, targets, CLI cases, docs
sections, and packaging steps are kept, the README support sentence and
integration help text now name all four agents, and the roadmap drops the
superseded "Cursor next" item. Full suite passes: 1563 tests, 0 failures.

Co-Authored-By: Claude Fable 5 <[email protected]>
The only conflict was the Unreleased changelog section; main's general
entries now lead Added, followed by the Cursor and OpenCode adapter
groups and main's Changed section. Full suite passes after the merge:
1583 tests, 0 failures.

Co-Authored-By: Claude Fable 5 <[email protected]>
@spaceamoeba-t
spaceamoeba-t merged commit 9520f69 into main Aug 12, 2026
2 checks passed
@spaceamoeba-t
spaceamoeba-t deleted the claude/cursor-opencode-adapters branch August 12, 2026 01:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant