feat(plugin): add opencode AI plugin with session-id tracking#33
Merged
Conversation
Second production AI pane type alongside claude-code. Tracks opencode
session-id rotation (new session, /new, fork, compaction) via a small JS
plugin loaded through OPENCODE_CONFIG_CONTENT at pane spawn. The plugin
lives entirely under $QUIL_HOME/opencodehook/ — no writes into
~/.config/opencode/ — and OPENCODE_CONFIG_CONTENT merges with the user's
existing opencode config (verified against opencode 1.14.x) so their
plugins, agents, and modes remain active.
Implementation mirrors internal/claudehook/ with measured hardening:
* O_NOFOLLOW on ReadPersistedSessionID closes the Lstat-then-Open
TOCTOU window (build-tagged constant; no-op on Windows where
symlink creation requires elevated privilege)
* BuildConfigContent rejects relative scriptPath so a non-absolute
QUIL_HOME cannot silently break tracking under prompts_cwd
* opencodeResumeTemplate shape-validates the recorded id via
opencodehook.IsValidSessionID (Go mirror of the JS plugin's regex)
before promoting — corrupted files cannot inject text into argv
* JS plugin caps hook.log at 1 MB with a single rotation,
de-duplicates writes so session.updated bursts don't thrash disk,
logs one "recorded <event-type> session=<id>" per id change
* Pane-id validation aligned on both sides via regex so a future
pane-id format change cannot silently disable tracking via
JS-only rejection
* Static-template resume args (--continue with empty PluginState)
pass through the restore-args gate via the new templateHasPlaceholder
helper, fixing a real bug where a fresh opencode pane closed
before its first session event respawned with empty args
Tests: 29 cases in internal/opencodehook/ (validation, install
idempotency, JSON round-trip, IsValidSessionID, symlink rejection,
relative-path rejection, ScriptPath) and 6 new cases in
internal/daemon/spawn_args_test.go (resume promotion, fallback paths,
shape-validation guard, templateHasPlaceholder, JSON parse-back).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Surfaces the new built-in plugin on the public site:
* plugins.ts gets an opencode entry with the new `beta?: boolean`
field — describes the OPENCODE_CONFIG_CONTENT injection model, the
no-touch-on-user-config guarantee, --session/--continue restore
behavior, and why --pure is deliberately not exposed as a toggle
* plugins.astro renders a BETA badge next to the kind tag when
`beta` is set; SEO meta + schema description updated from "four
built-in pane types" to "five" with opencode named
* index.astro § "Yours in 30 lines of TOML" body lists opencode as
the fifth built-in type with a (beta) marker
* features.ts "AI session resume" detail now mentions OpenCode (beta)
alongside Claude Code (production); "Typed panes" detail bumped to
five built-in pane types
`astro check` clean, `astro build` produces all 11 pages without warnings.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds opencode as the second production AI pane type alongside claude-code. Tracks opencode's session-id rotation (new session,
/new, fork, compaction) so panes resume to the same conversation across daemon restarts — the central value-prop of Quil for AI workflows.The trick: opencode has a real first-class plugin runtime (Bun-based, 25+ lifecycle events) that we hook via the
OPENCODE_CONFIG_CONTENTenv var. This means no writes into~/.config/opencode/— the plugin file lives entirely under$QUIL_HOME/opencodehook/.OPENCODE_CONFIG_CONTENTmerges with the user's existing opencode config (verifiedagainst opencode 1.14.x), so their plugins, agents, and modes remain active inside Quil-spawned opencode panes.
How it works
$QUIL_HOME/opencodehook/quil-session-tracker.jsviaopencodehook.EnsureScripts(atomic temp+rename, mirrorsclaudehook).QUIL_PANE_ID=<paneID>QUIL_HOME=<absolute-path>(absolutized at the boundary so a relativeQUIL_HOMEcannot silently break tracking)OPENCODE_CONFIG_CONTENT='{"plugin":["<abs-path-to-quil-session-tracker.js>"]}'session.created/session.updated/session.idle/session.compacted/session.deletedevents from opencode's bus, extractsevent.properties.sessionID(with fallback toevent.properties.info.idfor forward-compatibility with the SDK type defs), and atomically writes$QUIL_HOME/sessions/opencode-<paneID>.id.opencodehook.ReadPersistedSessionIDand promotes the resume args to["--session", "{session_id}"]; falls back to the plugin's configured["--continue"]if no id was recorded yet.Files
internal/opencodehook/(opencodehook.go, opencodehook_test.go, scripts/quil-session-tracker.js, o_nofollow_unix.go, o_nofollow_windows.go)internal/plugin/defaults/opencode.tomlinternal/daemon/daemon.go(EnsureScripts call, opencodeSpawnPrep, opencodeResumeTemplate, refactored resumeTemplateFor into per-plugin dispatch, templateHasPlaceholder helper)internal/daemon/spawn_args_test.go(6 new cases) + 29 cases in opencodehook_test.go.claude/CLAUDE.md,docs/plugin-reference.md,CHANGELOG.mdinternal/claudehook/claudehook.go(one comment improving cleanup intent).gitignore(added/quil-dev,/quild-dev,/quil-debug,/quild-debug)Hardening applied during review
ReadPersistedSessionIDusesO_NOFOLLOW(build-tagged constant for portability) instead of Lstat-then-Openopencodehook.IsValidSessionID(Go mirror of the JS plugin's regex) filters corrupted ids before they reach--session <arg>BuildConfigContentrejects relative paths so a non-absoluteQUIL_HOMEcannot silently break tracking whenprompts_cwdputs opencode in a user-chosen directory$QUIL_HOME/opencodehook/hook.logcapped at 1 MB with single rotation; writes de-duplicated by last recorded id sosession.updatedbursts during a single response don't thrash the disktypeof sessionId === "string"guard,quilHome.includes("\0")rejectionpaneIDReand JSPANE_ID_REaligned so a future pane-id format change cannot silently disable tracking via JS-only rejectiontemplateHasPlaceholder— fixes a real bug where session_scrape with emptyPluginStatedropped its--continuefallback on restoreValidateQuilDir's reject set — would otherwise truncate env vars silently on the C sideatomicWriteChmod-before-Write — closes the umask-derived Windows window where the temp file briefly has 0644Test plan
go vet ./...— cleango test ./...(full suite, 17 packages) — all greengo test -raceoninternal/opencodehook/,internal/daemon/,internal/claudehook/— cleanGOOS=windows go build ./...— clean (validateso_nofollow_unix.go/o_nofollow_windows.gobuild-tag split)node --check internal/opencodehook/scripts/quil-session-tracker.js— JS syntax valid$QUIL_HOME/opencodehook/OPENCODE_CONFIG_CONTENT(verified viaopencode debug configshowingsource: OPENCODE_CONFIG_CONTENT)--session <id>arg (verified in spawn log)--continue(verified in spawn log)--print-logstoggle preserved across restartCaveats / Future work
~/.local/share/opencode/opencode.db.opencode.tomlare copied from claude-code; should be tuned after dogfooding against opencode's actual prompts.--puretoggle deliberately not exposed — would disable external plugins including our tracker.