Skills and hooks for Codex, OpenCode and GitHub Copilot CLI - #586
Merged
Conversation
GenerateRouting emitted its own gortex:skills marker pair inside a payload every adapter re-wraps with the communities markers, so the block landed nested. It also advertised /gortex-<label> slash commands that exist only on Claude Code, telling every other host to invoke something it has no way to resolve, and named legacy one-tool-per- operation tools the public facade no longer exposes. The routing table now points at analyze(operation:"communities") and the skill bodies name only facade tools.
AppendInstructions had no call sites left — every adapter moved to UpsertMarkedBlock. Keeping it invited a new adapter author to copy a pattern that cannot update a block in place (it detects the sentinel and skips, so a changed body never reaches a file that already has one) and writes non-atomically.
audit_agent_config probed only the single-file rules surfaces, so path-scoped Copilot instructions and every skill tree were invisible to a drift audit — including the trees Gortex itself writes.
The standalone `copilot` binary had no adapter — the vscode adapter targets Copilot inside VS Code and never probes it. This registers gortex as an MCP server in ~/.copilot/mcp-config.json, writes the rule block into ~/.copilot/copilot-instructions.md, and shares the repo communities block with the vscode adapter. Two Copilot-specific facts the shared helpers do not cover: the MCP map is keyed "mcpServers" rather than VS Code's "servers", and each entry needs a "type" transport discriminator, which DefaultGortexMCPEntry omits because VS Code infers stdio. Copilot also discovers a repo .mcp.json ahead of the user config, so a gortex-authored entry there is reconciled in place — it otherwise shadows this adapter with an entry that has no type and a shell-style env value Copilot does not expand. The wizard now fails a test when a registered adapter has no label: without one the slug is displayed but silently dropped from the user's picks.
The 21 skill bodies are authored as Claude SKILL.md constants, and hermes was the only adapter reusing them — via private frontmatter surgery that the next three hosts would each have had to copy. skillpack parses one of those files into a neutral Skill and re-renders frontmatter from ordered pairs, so a host contributes only its own frontmatter shape. The package is stdlib-only and imports no adapter: a later skillpack.Sync has to be callable from claudecode, so an edge in the other direction would be a cycle. Hermes keeps a verbatim copy of the deleted helpers as a test oracle, which pins all 20 routing skills to byte-identical output.
Three files advertised how many integrations ship and all three had drifted — nineteen adapters where the registry had twenty, nineteen slash commands where there are twenty, nineteen skills where there are twenty-one. A count a reader can disprove costs more than no count. The new guard derives the total from buildRegistry(), so the next adapter fails until every page claiming a total is updated.
Copilot CLI's hook events are camelCase and its response fields are flat and per-event, not Claude's hookSpecificOutput envelope. The one that bites: userPromptSubmitted accepts only modifiedPrompt, so reusing Claude's lowering compiles, passes a shape test, and silently discards every byte of prompt-time context. Only the five events Gortex produces payloads for are handled; the other nine return empty at exit 0 rather than paying a subprocess spawn to say nothing. agentStop is deliberately silent — it carries decision and reason only, with no channel for a turn-end briefing. powershell normalises to Bash alongside bash. It is the shell tool on Windows, and a bash-only table would leave that whole surface unenforced. Event names are mapped to Claude's vocabulary before telemetry: hookEffectivenessEvents is a PascalCase allowlist that silently drops anything else, which would have made doctor report "configured but never ran" forever on a working install.
OpenCode has no lifecycle-hook configuration at all — a JS plugin is its only extension point — so it gets the same treatment Pi already had: a Gortex-owned event envelope on stdin, a decision on stdout. Rather than fork handlePi, PiEvent/PiDecision become BridgeEvent/ BridgeDecision (Pi keeps aliases) and both hosts share one core that folds host event names into three slots. The core now writes hook-effectiveness records, which the Pi bridge never did. Without a denominator, doctor reads a working bridge as a hook that has never fired. Pi gets the fix for free.
…ser scope renderOne pinned ModeProject with InstallHooks false and no generated skills, so no adapter's hook stanza, no per-community skill and no user-level artifact was pinned by CI — precisely the surfaces about to grow. Manifest keys are now mode-prefixed and each pass gets its own sandbox. Turning user-scope rendering on required closing two escapes first. InstructionsDir fell back to the real ~/.gortex/instructions, which also made the rendered skill set depend on the developer's active profile; CLAUDE_CONFIG_DIR let the whole user-scope write leave the sandbox, silently emptying that half of the manifest. Both are pinned, and a guard now fails the test if a render touches the real home — it was verified to fail when the pin is removed. The codex hook command is deterministic too: it interpolates GORTEX_CODEX_HOOK_MODE at render time, so the golden used to encode whichever posture the regenerating developer had exported.
Codex already had MCP, four lifecycle hooks and a rule block; skills were the gap. The curated pack goes to $HOME/.agents/skills in global mode and per-community skills to .agents/skills in the repo, matching claudecode's split so a plain init leaves the working tree clean. Codex does NOT read ~/.codex/skills — that tree is configuration and silently ignores a SKILL.md — so a positive-only test suite would pass over a feature that loads nothing. The negative assertion is a required test, not a nicety. Directories are never nested: Codex documents one directory per skill under .agents/skills and says nothing about recursing, and the generated DirName is already prefixed, so a grouping level would buy a silent no-op. $HOME/.agents/skills is also one of OpenCode's global skill roots, so this single write serves both hosts. The render is byte-stable and the writer skips unchanged files, so two adapters reaching the same path in one install run converge instead of churning.
Curated skills and subagents install to ~/.copilot; per-community skills to .github/skills in the repo. Home-level ~/.claude loading was removed in Copilot v1.0.36 and ~/.agents/skills in v1.0.66, so those paths would be a silent no-op and are asserted against. Hooks register only the four events Gortex produces payloads for, and each entry carries both a bash and a powershell command — Copilot hook entries take separate fields per shell, so a POSIX-only string leaves Windows users with a hook that never runs. The camelCase wire names stay confined to the config writer. Inspect reports Claude's PascalCase vocabulary because doctor tallies runs against a PascalCase allowlist, and keying it the other way would report a working install as never having fired. The subagent tools allowlist is omitted rather than translated: the Claude spelling is mcp__gortex__*, which Copilot does not resolve, and an unrecognised entry grants nothing instead of falling back to the full toolbox.
OpenCode had MCP and a routing block only. It now gets the curated skills and slash commands at user level, per-community skills in the repo, and — since it has no lifecycle-hook configuration at all — a JS plugin that shells the bridge protocol. The skill name is taken from the same value used as the directory name, so OpenCode's name-equals-directory rule is unrepresentable to violate rather than merely tested. The plugin installs to ~/.config/opencode/plugin/gortex.js, not into the repo. It is the only executable artifact Gortex writes, and the repo-level plugin dir is committed, so a repo install would push a file that shells out on every tool call onto every teammate who clones. The singular plugin/ spelling is deliberate: that glob alone does not recurse. HookEvents omits PostToolUse because tool.execute.after never shells the bridge — declaring it would spend a subprocess per tool call and still report a permanent "configured but never ran" blocker.
… exist Adds the copilot-cli row, records the skill/command/hook paths Codex, OpenCode and Copilot CLI now receive, and disambiguates copilot-cli from the vscode adapter and the copilot LLM provider. The per-host table states the NOs explicitly, because each is a vendor limit a reader would otherwise file as a Gortex omission: Codex has no slash-command surface worth targeting, Copilot CLI does not read prompt files, OpenCode has no hook system at all, and Copilot's agentStop carries no context channel. The helper list was three entries out of date and omitted most of what adapters actually write through, including the marked-block upsert every instructions surface depends on.
Adds the ~/.copilot sentinel and the copilot-cli expectation, and notes that the list is hand-maintained so the next adapter is covered only when both places are updated.
Profile.Skills was honoured only by claudecode, so switching to a three-skill profile left the full pack installed everywhere else — and the three hosts that just gained curated packs inherited the same gap. skillpack.Sync is a faithful port of the claudecode rule, because the rule is what decides which files get deleted from a user's machine on an upgrade: prune only when the skill is outside the profile AND its bytes are still one we shipped. A customised copy is kept with a warning on every pass, not just the first. A host with no installed skills root is a no-op rather than an install, so a switch cannot conjure a Codex or Copilot surface on a machine that never ran gortex install. The call site stays in cmd/gortex: routing hosts through claudecode would invert the direction skillpack exists to protect.
…mpty home Hermes was the first adapter to reuse the Claude skill bodies and the last to learn about profiles, so a switch to a three-skill profile left all twenty routing skills installed — the same machine enforced two different profiles depending on which agent the user opened. Hermes files skills under a category directory rather than directly under the root, so the reconciler takes an optional per-skill directory resolver. Its native master skill is absent from the routing set, which is what keeps it out of the subset comparison and alive across every profile. Codex's curated install now refuses an unresolved home instead of joining onto it: the relative path lands the pack in the process's working directory, which is usually the user's repo — the one place this pack must never go.
doctorAgentProbes is hand-maintained and covered codex and claude-code only, so the other hosts reported nothing at all. A guard now scans the adapter tree for Inspect functions and fails when one has no probe, so the next host is not silently forgotten. The probe key has to be the same string the hook writer bakes into --agent, or the activity lane reads zero forever on a working install; both sides now share the adapter's Name constant and two tests pin it. Hook lines used to print only when the host's config file was present. OpenCode's hook surface is a plugin that lives nowhere near its config, so a fully wired install rendered as a single complaint about a missing opencode.json.
Every surface added for Codex, OpenCode and Copilot CLI survived uninstall, including the OpenCode bridge plugin — which keeps shelling out to a binary the same command may have just removed. Deletion is by evidence, never by path. The skill trees are shared with the user's own skills, so an entry is removed only when its bytes still match what we shipped, or for generated skills when it carries the gortex- prefix the generator enforces. A customised copy is kept and reported. Directory pruning uses os.Remove, which refuses a non-empty directory. Preview and removal now read one table, so the wizard cannot promise a deletion that will not happen. That also fixes a live bug: the --global branch was gated on Claude Code having artifacts, so a machine with only OpenCode configured was told there was nothing to uninstall.
.claude/skills/generated survived a repo uninstall. Unlike the shared skill trees it needs no per-entry ownership check: the generated/ level exists precisely because nothing but Gortex writes there.
Keys are now mode-prefixed and every adapter renders twice, so the diff is large but mechanical. What is worth reading: codex and opencode gain skill trees, copilot-cli arrives, and every adapter's hook stanza and per-community skills are pinned for the first time.
Pre-existing comment misalignment in a file this branch already touches; leaving half a file unformatted is worse than the small unrelated hunk.
Codex has a real delegation surface at ~/.codex/agents/*.toml, so the two graph-only sub-agents Claude Code gets now install there too. The file format has two ways to fail that leave nothing behind, both confirmed against a real Codex install before writing a line: Codex deserializes an agent file with deny_unknown_fields, so a single unrecognised key rejects the whole agent — the emitted key set is therefore exactly name, description, sandbox_mode and developer_instructions, and a test pins it. mcp_servers is a table of full server definitions, not a list of server names. The intuitive mcp_servers = ["gortex"] fails to deserialize and voids the entire file. It is omitted: omission inherits the session's servers, and Codex has no allowlist semantic, so there was nothing to gain by emitting it. sandbox_mode = "read-only" turns both agents' written promise not to mutate files into a sandbox guarantee. Instructions use a TOML literal string so a backslash in a future body cannot become an escape. User-level only: project-scoped .codex/agents is skipped entirely unless the project is trusted, and the curated artifacts in this branch all stay out of the user's working tree.
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.
A community user runs Claude Code, Codex, OpenCode and GitHub Copilot CLI, and gets the full Gortex integration in exactly one of them. This closes that.
Each host turned out to need something different, and two are more capable than the gap suggests:
codexopencodecopilot-cliThe existing
vscodeadapter targets Copilot inside VS Code and never probes thecopilotbinary, so the standalone CLI had no adapter. It is now the twentieth.Vendor facts that shape the code
Each of these is a place where the obvious implementation ships something that tests green and loads nothing:
~/.codex/skills. That tree is configuration. Skills live in.agents/skills,$HOME/.agents/skillsand/etc/codex/skills. A negative test asserts we never write to the ignored paths.~/.agents/skillsis a shared cross-agent root that Codex and OpenCode both scan, so one write serves both. Copilot CLI dropped it in v1.0.66 and needs~/.copilot/skills; it also stopped reading home-level~/.claude/*in v1.0.36.namediffers from its directory. The name is taken from the same value used as the directory, so the rule is unrepresentable to violate.userPromptSubmittedaccepts onlymodifiedPrompt. Reusing Claude'sadditionalContextlowering compiles, passes a shape test, and discards the whole payload.bashandpowershellcommands, andpowershellis a first-class tool name. A POSIX-only entry leaves the entire Windows surface unenforced.agentStophas no context channel, so there is no turn-end briefing on that host — recorded as a stop-line rather than faked..mcp.jsonahead of its user config. Gortex already writes one, without the requiredtypeand with a shell-style env value Copilot does not expand, so a stale entry shadowed the new adapter. It is reconciled in place.deny_unknown_fields), and itsmcp_serversfield is a table of full server definitions, not a list of names — the intuitivemcp_servers = ["gortex"]fails to deserialize and voids the entire agent. Four keys are emitted andmcp_serversis omitted, which is documented to inherit the session's servers. Codex has no per-agent allowlist, so there was nothing to gain by emitting it.Deliberate calls worth reviewing
claudecodealready does, sogortex initnever drops 21 host-agnostic markdown files into a working tree for teammates to review.~/.config/opencode/plugin/gortex.js, not.opencode/. It is the only executable artifact Gortex writes and the repo-level directory is committed — a repo install would push a file that shells out on every tool call onto everyone who clones. It is gated onInstallHooks, so--no-hooksturns it off, anduninstallremoves it.~/.copilot, not.github/..github/hooks/and.github/skills/are committed and would run for every teammate.skillpackimports no adapter package. A laterskillpack.Synchad to be callable fromclaudecode, so an edge the other way is a cycle. The rule is stated in the package doc.Bugs found and fixed on the way
/gortex-<label>slash commands that exist on no host but Claude Code.uninstall --globalwas gated on Claude Code having artifacts, so a machine with only OpenCode configured was told there was nothing to uninstall.go test ./cmd/gortexwould have written to the developer's real home; a guard now fails the test if a render touches it, and it was verified to fail when the pin is removed.GORTEX_CODEX_HOOK_MODEat render time, so the golden encoded whichever posture the regenerating developer had exported.hookEffectivenessEventsis a PascalCase allowlist that silently drops anything else, which would have madedoctorreport "configured but never ran" forever on a working Copilot install.gortex instructions switchreshaped only Claude Code's skills. Hermes had 20 skills that never shrank, and the three new hosts would have inherited the same gap.docs/agents.md,docs/skills.mdandREADME.mdwere all stale; a guard now derives them frombuildRegistry().agents.AppendInstructionsremoved — it could not update a block in place and wrote non-atomically, and a new adapter author would have copied it.Verification
go build ./...,go vet ./...,golangci-lint,gofmt— clean.go test ./...— 184 packages, zero failures, exit 0.go test -raceon every touched package — clean.Removal is by evidence, never by path: the skill trees are shared with the user's own skills, so an entry is deleted only when its bytes still match what we shipped. A customised copy is kept and reported, on install, on profile switch and on uninstall.
Not done, and why
No manual verification against the real binaries. Several of these surfaces can pass every unit test while loading nothing, because the failure mode is a path or a key the host silently ignores. Each needs one run against the real CLI before this is trusted in the wild: Codex
/skillslisting the pack, Copilot denying a grep with a reason that names the Gortex tool, OpenCode blocking the same grep with the bridge installed.For Codex there is a real oracle worth using:
codex doctorreports a rejected agent file as a startup warning mentioningagent role, while the exit code stays 0 and everything else loads.codex doctor 2>&1 | grep -i "agent role"should print nothing.OpenCode sub-agents. Their
toolsmap is keyed by tool name and the spelling for an MCP-served tool is unverified; a wrong key silently grants nothing.Repo-level
AGENTS.md/CLAUDE.mdrouting blocks are still not stripped byuninstall— pre-existing, and it affects every adapter, so it does not belong in this branch.