Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .changeset/correct-subagent-event-support.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"agent-bundle": minor
---

Correct Claude Code and Codex event-route capabilities for native
`SubagentStart` and `SubagentStop` hooks, including host-specific input
validation, result projection, plugin packaging, and pinned Codex wire-schema
evidence. Resolve the actual Claude or Codex invoker before a composite plugin
route validates input or projects output.
19 changes: 19 additions & 0 deletions examples/rsc-agent-runtime/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,25 @@ Host/Origin allowlists mitigate DNS rebinding and cross-origin requests, but the
| Claude Code | Native package contract: `${CLAUDE_PLUGIN_ROOT}` MCP/hook paths and `Write|Edit` hook | Real host run is a skip-gated manual opt-in; no attached native evidence in this snapshot; never an iframe renderer |
| Codex CLI | Native package contract: marketplace, relative MCP path, `${PLUGIN_ROOT}` hook, and `apply_patch` matcher | Real host run is a skip-gated manual opt-in; no attached native evidence in this snapshot; native PostToolUse/shared state remains unproven under `exec --ephemeral` |

### Semantic event-route support

| Event family | Cursor | Claude Code 2.1.250 | Codex 0.147.0 |
| --- | --- | --- | --- |
| `session/start` | Supported | `SessionStart` | `SessionStart` |
| `tool/before` | Supported | `PreToolUse` | `PreToolUse` |
| `tool/after` | Supported | `PostToolUse` | `PostToolUse` |
| `stop` | Supported | `Stop` | `Stop` |
| `agent/start` | `subagentStart` | `SubagentStart` | `SubagentStart` |
| `agent/stop` | `subagentStop` | `SubagentStop` | `SubagentStop` |
| `workspace/open` | `workspaceOpen` | Unavailable | Unavailable |

`agent/start` is context-injection-only on Claude Code and Codex and cannot
block subagent creation. Their `agent/stop` routes can continue the subagent
with the native `decision: "block"` plus `reason` contract. Codex
`SubagentStop` exit-0 output is always JSON; its generated 0.147.0 output
schema has no `additionalContext` field, so the route projection rejects that
unsupported effect rather than silently fabricating one.

## Extension-author guide

The definition and lowerer keep serializable namespaced descriptor, resource, and result `_meta` opaque. Add a vendor extension as `_meta["vendor.example/feature"]`, merge it with `mergeSerializableMetadata`, and keep it out of model-visible text unless it belongs there. The optional Claude resource domain is added only to `resources/read` content metadata when an explicit public MCP URL is supplied; it is not added to resource registration by default.
Expand Down
1 change: 1 addition & 0 deletions examples/rsc-agent-runtime/rstest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export default defineConfig({
// swept into this plain Node pool where no test manifest is registered.
exclude: ['tests/route-unit/**'],
include: ['tests/**/*.test.{ts,tsx}'],
exclude: ['tests/route-unit/**/*.test.{ts,tsx}'],
pool: { maxWorkers: 1 },
testEnvironment: 'node',
// Every suite here runs real rsbuild compiles and spawned children, which a
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,16 @@
"hooks": {
"config": "hooks/hooks.json",
"events": {
"agentStart": "SubagentStart",
"agentStop": "SubagentStop",
"afterTool": "PostToolUse",
"beforeTool": "PreToolUse",
"sessionStart": "SessionStart",
"stop": "Stop"
},
"eventRoutes": {
"agent/start": {
"reason": "The pinned Claude Code 2.1.250 hooks contract has no subagent-start event.",
"state": "unavailable"
},
"agent/stop": {
"reason": "The pinned Claude Code 2.1.250 hooks contract has no subagent-stop event.",
"state": "unavailable"
},
"agent/start": { "nativeEvent": "SubagentStart", "state": "supported" },
"agent/stop": { "nativeEvent": "SubagentStop", "state": "supported" },
"session/start": { "nativeEvent": "SessionStart", "state": "supported" },
"stop": { "nativeEvent": "Stop", "state": "supported" },
"tool/after": { "nativeEvent": "PostToolUse", "state": "supported" },
Expand Down Expand Up @@ -92,7 +88,11 @@
"Codex and Cursor publish no plugin LSP surface at their pinned revisions, so the unified bundle's .lsp.json reaches Claude Code only.",
"Plugin developer tools reference: `claude plugin validate <dir>` checks plugin.json, hooks/hooks.json, and default-directory Skill, agent, and command frontmatter; manifest-less component directories require 2.1.233 or later.",
"`claude plugin validate <dir> --strict` promotes tolerated warnings such as unrecognized or near-miss fields and non-object experimental/metadata values to exit failure; the reference recommends strict mode in CI.",
"Development tools include `claude --plugin-dir <dir> plugin list --json` for registration proof, `claude plugin details <name>` for component inventory and host-owned token estimates, `claude plugin tag`, and `claude --debug` for loading diagnostics."
"Development tools include `claude --plugin-dir <dir> plugin list --json` for registration proof, `claude plugin details <name>` for component inventory and host-owned token estimates, `claude plugin tag`, and `claude --debug` for loading diagnostics.",
"https://code.claude.com/docs/en/hooks documents SubagentStart when Agent spawns a subagent and SubagentStop when it finishes; both match agent_type, including anchored plugin-scoped identifiers such as ^my-plugin:reviewer$.",
"SubagentStart adds agent_id and agent_type to common hook fields. It cannot block subagent creation; hookSpecificOutput.additionalContext injects context before the first subagent prompt, and exit-2 stderr is only a non-blocking notice in the subagent transcript.",
"SubagentStop adds stop_hook_active, agent_id, agent_type, agent_transcript_path, and last_assistant_message. decision:block plus reason or exit 2 keeps the subagent running; hookSpecificOutput.additionalContext provides non-error feedback that also continues it.",
"The hooks reference states prompt_id requires Claude Code 2.1.196 or later; the pinned 2.1.250 release covers that input field without a version bump."
]
}
}
27 changes: 19 additions & 8 deletions packages/agent-bundle/src/adapters/capabilities/codex-0.147.0.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,16 @@
"hooks": {
"config": "hooks/hooks.json",
"events": {
"agentStart": "SubagentStart",
"agentStop": "SubagentStop",
"afterTool": "PostToolUse",
"beforeTool": "PreToolUse",
"sessionStart": "SessionStart",
"stop": "Stop"
},
"eventRoutes": {
"agent/start": {
"reason": "The pinned Codex 0.147.0 hooks contract has no subagent-start event.",
"state": "unavailable"
},
"agent/stop": {
"reason": "The pinned Codex 0.147.0 hooks contract has no subagent-stop event.",
"state": "unavailable"
},
"agent/start": { "nativeEvent": "SubagentStart", "state": "supported" },
"agent/stop": { "nativeEvent": "SubagentStop", "state": "supported" },
"session/start": { "nativeEvent": "SessionStart", "state": "supported" },
"stop": { "nativeEvent": "Stop", "state": "supported" },
"tool/after": { "nativeEvent": "PostToolUse", "state": "supported" },
Expand Down Expand Up @@ -46,5 +42,20 @@
"pluginData": false,
"pluginRoot": "relative-with-plugin-root-cwd",
"workspaceRoot": false
},
"provenance": {
"observedAt": "2026-09-01",
"source": "https://learn.chatgpt.com/docs/hooks",
"evidence": [
"The Codex 0.147.0 hooks reference lists SubagentStart and SubagentStop as lifecycle events whose matcher filters agent_type; subagent hook session_id is the parent session id.",
"SubagentStart adds turn_id, agent_id, agent_type, and permission_mode. Plain stdout or hookSpecificOutput.additionalContext adds developer context to the subagent; continue:false is parsed but does not stop creation.",
"SubagentStop adds turn_id, agent_id, agent_type, agent_transcript_path, stop_hook_active, and last_assistant_message. Exit-0 stdout must be JSON; decision:block plus reason or exit 2 continues the subagent flow, while continue:false takes precedence.",
"Codex discovers enabled plugin hooks at plugin-root hooks/hooks.json by default or from a ./-prefixed path, path array, inline object, or inline-object array in .codex-plugin/plugin.json. Paths must remain inside the plugin root; installation does not auto-trust non-managed hooks.",
"Plugin hook commands receive PLUGIN_ROOT and PLUGIN_DATA plus CLAUDE_PLUGIN_ROOT and CLAUDE_PLUGIN_DATA compatibility aliases.",
"Pinned generated schema: https://github.com/openai/codex/blob/rust-v0.147.0/codex-rs/hooks/schema/generated/subagent-start.command.input.schema.json (sha256 ce7dc9b5ae8826d1e0c59ffcea793e558aebceb7917a2eb9bb2edd8a7ac37aa9).",
"Pinned generated schema: https://github.com/openai/codex/blob/rust-v0.147.0/codex-rs/hooks/schema/generated/subagent-start.command.output.schema.json (sha256 34e8ec95393d2aa930d7932a34c3fb29a5e5f90c264fdbcc581393c5838b4660).",
"Pinned generated schema: https://github.com/openai/codex/blob/rust-v0.147.0/codex-rs/hooks/schema/generated/subagent-stop.command.input.schema.json (sha256 94dc8df29f4691195ac2338ae6de876230e5100a10b94ef48df4e732424b5df5).",
"Pinned generated schema: https://github.com/openai/codex/blob/rust-v0.147.0/codex-rs/hooks/schema/generated/subagent-stop.command.output.schema.json (sha256 8ba2cd7899ae4544193764e67e988235edebe984abe5788634d123bbf13e3e3a)."
]
}
}
7 changes: 4 additions & 3 deletions packages/agent-bundle/src/adapters/claude.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,8 @@ const hookContract = Object.freeze({
capabilityRevision: capabilityTable.observedCliVersion,
commandRoot: '${CLAUDE_PLUGIN_ROOT}',
encodePlaygroundInput: encodeNativeHookPlaygroundInput,
encodePlaygroundOutput: encodeNativeHookPlaygroundOutput,
encodePlaygroundOutput: (result, event, nativeEvent) =>
encodeNativeHookPlaygroundOutput(result, event, nativeEvent, 'claude'),
eventNames: capabilityTable.hooks.events,
eventRouteNames: supportedEventRouteNamesFrom(capabilityTable.hooks.eventRoutes),
manifestPath: 'hooks/hooks.json',
Expand All @@ -131,9 +132,9 @@ const hookContract = Object.freeze({
wrapperSource: (entry) => nativeHookWrapperSource(entry, 'Claude'),
} satisfies TargetHookContract);
const metadata = Object.freeze({
adapterRevision: '1.2.0',
adapterRevision: '1.3.0',
capabilityRevision: capabilityTable.observedCliVersion,
capabilitySha256: '13bc41224c5343b33d259986a66feb279e15431c5019bb2a1c443eaa60e9a9ea',
capabilitySha256: 'a9c8821ee5cbc6aef65816c5025170389fd490b6d1c4e4e893599aa6a36f2265',
observedVersion: capabilityTable.observedCliVersion,
schemas: schemaDescriptorsFrom(schemaProvenance, schemaProvenance.observedCliVersion),
});
Expand Down
7 changes: 4 additions & 3 deletions packages/agent-bundle/src/adapters/codex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,8 @@ const hookContract = Object.freeze({
capabilityRevision: capabilityTable.observedCliVersion,
commandRoot: '${PLUGIN_ROOT}',
encodePlaygroundInput: encodeNativeHookPlaygroundInput,
encodePlaygroundOutput: encodeNativeHookPlaygroundOutput,
encodePlaygroundOutput: (result, event, nativeEvent) =>
encodeNativeHookPlaygroundOutput(result, event, nativeEvent, 'codex'),
eventNames: capabilityTable.hooks.events,
eventRouteNames: supportedEventRouteNamesFrom(capabilityTable.hooks.eventRoutes),
manifestPath: 'hooks/hooks.json',
Expand All @@ -118,9 +119,9 @@ const hookContract = Object.freeze({
wrapperSource: (entry) => nativeHookWrapperSource(entry, 'Codex'),
} satisfies TargetHookContract);
const metadata = Object.freeze({
adapterRevision: '1.1.0',
adapterRevision: '1.2.0',
capabilityRevision: capabilityTable.observedCliVersion,
capabilitySha256: 'f2c7109e3572ebde8739e08f6fdfa7a7b5d7817e3e216406b9c855f1570e2bd9',
capabilitySha256: '44e697be71a29db9ec029ed7d9eb8807b90e95d6a15f3a71a47148125c902194',
observedVersion: capabilityTable.observedCliVersion,
schemas: schemaDescriptorsFrom(schemaProvenance, schemaProvenance.observedCliVersion),
});
Expand Down
Loading
Loading