@iamdevlinph/codex-kit 1.0.4 → 1.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  Portable Codex setup for new devices and multiple projects. It provides:
4
4
 
5
5
  - four custom subagents: explorer, quick implementer, implementer, and reviewer
6
- - global subagent-routing guidance without a `commit-pusher`
6
+ - automatic global role routing
7
7
  - a reusable, stack-neutral `AGENTS.md` template
8
8
  - safe commands for global setup, project synchronization, and reconciliation
9
9
 
@@ -20,17 +20,30 @@ codex-kit global install
20
20
  codex-kit global configure
21
21
  ```
22
22
 
23
+ Start a new Codex task or restart the client after installation. Codex may ask
24
+ you to review and trust the new command hooks once; approve them after confirming
25
+ that they run the installed `${CODEX_HOME:-~/.codex}/codex-kit/routing-hook.js`.
26
+
23
27
  `npm install --global @iamdevlinph/codex-kit@latest` works as an alternative.
24
28
  For one-off use without a global installation, prefix a command with
25
29
  `pnpm dlx @iamdevlinph/codex-kit@latest`.
26
30
 
27
- `global install` copies reusable agents to `${CODEX_HOME:-~/.codex}` and
28
- maintains a marked routing section in the global `AGENTS.md`.
31
+ `global install` copies reusable agents to `${CODEX_HOME:-~/.codex}`, maintains
32
+ a marked routing section in the global `AGENTS.md`, and adds package-owned
33
+ handlers to the global `hooks.json` without replacing existing hooks.
34
+
35
+ The Sol root plans, routes, coordinates, and validates. On every prompt, the
36
+ routing hook supplies the current `SUBAGENT_ROUTING.md`; the root classifies the
37
+ task and delegates to the exact matching role. The role's agent TOML—not the
38
+ routing policy—selects its model and reasoning effort. Planning, conversation,
39
+ status, and small read-only checks may stay on the root.
29
40
 
30
- The Sol root agent plans, coordinates, and validates. All project-file changes,
31
- including small one-file edits, are delegated to a Luna `quick-implementer` or
32
- `implementer`; the selected implementation agent edits directly without further
33
- delegation.
41
+ The hook rejects direct root writes through Codex edit tools and obvious
42
+ write-shaped shell commands. `SubagentStart` opens a 15-minute write allowance
43
+ for that worker's `session_id + turn_id`; `SubagentStop` closes it. This identifies
44
+ the delegated lane without hard-coding a model. Codex hooks do not intercept
45
+ every possible indirect shell-write path, so this is a workflow guardrail rather
46
+ than a security boundary.
34
47
 
35
48
  `global configure` sets these defaults while preserving unrelated settings:
36
49
 
@@ -60,8 +73,8 @@ pnpm dlx @iamdevlinph/codex-kit@latest global list
60
73
  ```
61
74
 
62
75
  The summary shows the Codex home, orchestrator, normal and Plan-mode reasoning
63
- effort, routing status, and installed custom agents without dumping unrelated
64
- configuration.
76
+ effort, routing-file and routing-hook status, and installed custom agents without
77
+ dumping unrelated configuration.
65
78
 
66
79
  Uninstall package-managed global files:
67
80
 
@@ -69,8 +82,8 @@ Uninstall package-managed global files:
69
82
  pnpm dlx @iamdevlinph/codex-kit@latest global uninstall
70
83
  ```
71
84
 
72
- Modified managed files are preserved unless `--force` is supplied. An existing
73
- unmanaged `commit-pusher.toml` is reported but never silently deleted.
85
+ Uninstall removes only codex-kit's handlers from `hooks.json` and preserves other
86
+ hooks. Modified managed files are preserved unless `--force` is supplied.
74
87
 
75
88
  ## Commands
76
89
 
@@ -180,7 +193,7 @@ commands do not add network latency or depend on registry availability.
180
193
  ## Requirements
181
194
 
182
195
  - Node.js 20 or newer
183
- - Codex with custom subagent support
196
+ - Codex with custom subagent and lifecycle-hook support
184
197
 
185
198
  The published package has no runtime dependencies and uses only Node.js
186
199
  standard-library modules.
@@ -194,3 +207,6 @@ redistribute the package beyond applicable law and npm's service terms.
194
207
 
195
208
  - [Codex custom subagents](https://learn.chatgpt.com/docs/agent-configuration/subagents)
196
209
  - [Codex `AGENTS.md` discovery](https://learn.chatgpt.com/docs/agent-configuration/agents-md)
210
+ - [Codex hooks](https://learn.chatgpt.com/docs/hooks)
211
+ - [Delegate Mode](https://github.com/Raylinkh/delegate-mode), whose short-lived
212
+ subagent allowance pattern informed codex-kit's write guard
@@ -1,42 +1,43 @@
1
1
  # Subagent Routing
2
2
 
3
- The root parent is the Sol planner and orchestrator. It must delegate all
4
- file-changing implementation to a Luna implementation agent. The user does not
5
- need to request subagents explicitly.
3
+ The root parent is the planner and orchestrator. Before acting on a substantive
4
+ task, classify the work using this file. Route by role and task shape, never by a
5
+ model name; each agent definition owns its model and reasoning effort.
6
6
 
7
- The parent owns requirements, architecture, integration, and final validation.
8
- Never delegate the overall objective. Avoid parallel write-heavy work and never
9
- assign overlapping files to multiple agents.
10
-
11
- For any task that creates, edits, or deletes project files, delegate the work to
12
- exactly one of `quick-implementer` or `implementer`. Include bookkeeping commands
13
- that write project state in that assignment. The parent may perform read-only
14
- inspection and final validation, but must not make the project-file changes itself.
15
-
16
- These routing rules apply to the root parent. An implementation subagent performs
17
- its assigned edits directly and must not delegate them again. Do not delegate
18
- trivial conversation or straightforward read-only commands. A slow command alone
19
- is not a reason to delegate.
20
-
21
- For delegated work:
22
-
23
- - Prefer one subagent. Add more only for independent, non-overlapping work.
24
- - Give each agent a bounded task and request a concise, decision-ready report.
25
- - For parallel implementation, assign explicit file or module ownership.
26
- - Wait for relevant agents and synthesize their results in the parent task.
27
- - Reuse prior exploration and evidence instead of repeating it.
7
+ The parent owns requirements, architecture, sequencing, integration, and final
8
+ validation. It may handle planning, conversation, status reporting, and small
9
+ read-only checks directly. When a route below matches, spawn that exact role
10
+ before performing the role's work. The user does not need to request delegation.
28
11
 
29
12
  Select custom agents by exact name:
30
13
 
31
- - Broad repository discovery or contract tracing: `code-explorer`
32
- - Mechanical, well-specified one- or two-file change: `quick-implementer`
33
- - Multi-file behavior change, debugging, or substantial tests: `implementer`
34
- - Independent review of high-risk or difficult-to-validate changes: `code-reviewer`
35
-
36
- Use either `quick-implementer` or `implementer` for a change, not both. Both roles
37
- run Luna; use the lowest reasoning effort that reliably fits the work. Do not
38
- substitute a generic agent when a matching custom role is available.
14
+ - Broad repository discovery, contract tracing, or search across many files:
15
+ `code-explorer`
16
+ - Mechanical, well-specified implementation localized to one or two files:
17
+ `quick-implementer`
18
+ - Multi-file behavior changes, debugging, migrations, or substantial tests:
19
+ `implementer`
20
+ - Independent review of security-sensitive, architectural, public-API,
21
+ concurrency, migration, or otherwise difficult-to-validate changes:
22
+ `code-reviewer`
23
+
24
+ For tasks with multiple phases, sequence only the roles that add value. For
25
+ example, use `code-explorer` before implementation only when broad discovery is
26
+ actually needed, and use `code-reviewer` after implementation only when the
27
+ change meets its risk threshold. Avoid parallel write-heavy work and never assign
28
+ overlapping files to multiple agents.
29
+
30
+ An assigned implementation agent performs its edits directly and must not
31
+ delegate them again. Use either `quick-implementer` or `implementer` for one
32
+ implementation slice, not both. Read-only agents never modify files.
33
+
34
+ For every delegation:
35
+
36
+ - Give the agent a bounded task and the relevant requirements.
37
+ - Request a concise, decision-ready result.
38
+ - Wait for the result before integrating or validating dependent work.
39
+ - Reuse returned evidence instead of repeating the same exploration.
39
40
 
40
41
  There is no commit-pusher role. Never delegate Git publishing. Do not stage,
41
- commit, or push unless the user explicitly requests that operation in the current
42
- task; if requested, the parent handles it after review.
42
+ commit, tag, or push unless the user explicitly requests that operation in the
43
+ current task; if requested, the parent handles it after review.
@@ -1,11 +1,11 @@
1
1
  name = "quick-implementer"
2
- description = "Low-cost agent for small, mechanical, well-specified changes in one or two files. Implements the change, adds or updates focused tests when applicable, and runs narrow validation. Escalates ambiguous or architectural work instead of guessing."
2
+ description = "Focused agent for small, mechanical, well-specified changes in one or two files. Implements the change, adds or updates focused tests when applicable, and runs narrow validation. Escalates ambiguous or architectural work instead of guessing."
3
3
  model = "gpt-5.6-luna"
4
- model_reasoning_effort = "low"
4
+ model_reasoning_effort = "medium"
5
5
  developer_instructions = """
6
6
  # Quick Implementer
7
7
 
8
- Begin your first user-visible response with this progress message exactly once: `Delegating to custom quick-implementer — gpt-5.6-luna, low reasoning.`
8
+ Begin your first user-visible response with this progress message exactly once: `Delegating to custom quick-implementer — gpt-5.6-luna, medium reasoning.`
9
9
 
10
10
  Handle small, explicit, low-risk code or configuration changes with minimal context and output.
11
11
 
package/bin/codex-kit.js CHANGED
@@ -10,6 +10,7 @@ const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
10
10
  const ASSETS = join(ROOT, "assets");
11
11
  const AGENTS_DIR = join(ASSETS, "agents");
12
12
  const ROUTING_FILE = join(ASSETS, "SUBAGENT_ROUTING.md");
13
+ const ROUTING_HOOK_FILE = join(ROOT, "bin", "routing-hook.js");
13
14
  const TEMPLATE_FILE = join(ASSETS, "TEMPLATE_AGENTS.md");
14
15
  const PACKAGE = JSON.parse(readFileSync(join(ROOT, "package.json"), "utf8"));
15
16
  const GLOBAL_BEGIN = "<!-- BEGIN codex-kit:subagent-routing -->";
@@ -44,6 +45,116 @@ function write(file, data) {
44
45
  writeFileSync(temporary, data);
45
46
  renameSync(temporary, file);
46
47
  }
48
+ function readJsonObject(file) {
49
+ if (!existsSync(file))
50
+ return {};
51
+ try {
52
+ const value = JSON.parse(readText(file));
53
+ if (isRecord(value))
54
+ return value;
55
+ }
56
+ catch {
57
+ // Use the actionable error below for invalid JSON and non-object roots.
58
+ }
59
+ throw new Error(`${file} must contain a JSON object; fix or move it before installing.`);
60
+ }
61
+ function shellQuote(value) {
62
+ return `'${value.replaceAll("'", `'"'"'`)}'`;
63
+ }
64
+ function hookCommands(file) {
65
+ return {
66
+ command: `/usr/bin/env node ${shellQuote(file)}`,
67
+ commandWindows: `node ${JSON.stringify(file)}`,
68
+ };
69
+ }
70
+ function removeHookHandlers(root, state) {
71
+ const hooks = root.hooks;
72
+ if (!isRecord(hooks))
73
+ return;
74
+ for (const [event, groupsValue] of Object.entries(hooks)) {
75
+ if (!Array.isArray(groupsValue))
76
+ continue;
77
+ const groups = groupsValue.flatMap((groupValue) => {
78
+ if (!isRecord(groupValue) || !Array.isArray(groupValue.hooks))
79
+ return [groupValue];
80
+ const handlers = groupValue.hooks.filter((handler) => {
81
+ if (!isRecord(handler))
82
+ return true;
83
+ return handler.command !== state.command && handler.commandWindows !== state.commandWindows;
84
+ });
85
+ return handlers.length ? [{ ...groupValue, hooks: handlers }] : [];
86
+ });
87
+ if (groups.length)
88
+ hooks[event] = groups;
89
+ else
90
+ delete hooks[event];
91
+ }
92
+ }
93
+ function installRoutingHooks(home, previous) {
94
+ const target = join(home, "hooks.json");
95
+ const hookFile = join(home, "codex-kit", "routing-hook.js");
96
+ const commands = hookCommands(hookFile);
97
+ const created = previous?.created ?? !existsSync(target);
98
+ const root = readJsonObject(target);
99
+ if (previous)
100
+ removeHookHandlers(root, previous);
101
+ const hooks = isRecord(root.hooks) ? root.hooks : {};
102
+ root.hooks = hooks;
103
+ const handler = {
104
+ type: "command",
105
+ command: commands.command,
106
+ commandWindows: commands.commandWindows,
107
+ timeout: 5,
108
+ };
109
+ const promptGroups = Array.isArray(hooks.UserPromptSubmit) ? hooks.UserPromptSubmit : [];
110
+ const toolGroups = Array.isArray(hooks.PreToolUse) ? hooks.PreToolUse : [];
111
+ hooks.UserPromptSubmit = [
112
+ ...promptGroups,
113
+ { hooks: [{ ...handler, statusMessage: "Loading subagent routing" }] },
114
+ ];
115
+ const startGroups = Array.isArray(hooks.SubagentStart) ? hooks.SubagentStart : [];
116
+ hooks.SubagentStart = [
117
+ ...startGroups,
118
+ { hooks: [{ ...handler, statusMessage: "Opening delegated write lane" }] },
119
+ ];
120
+ const stopGroups = Array.isArray(hooks.SubagentStop) ? hooks.SubagentStop : [];
121
+ hooks.SubagentStop = [
122
+ ...stopGroups,
123
+ { hooks: [{ ...handler, statusMessage: "Closing delegated write lane" }] },
124
+ ];
125
+ hooks.PreToolUse = [
126
+ ...toolGroups,
127
+ {
128
+ matcher: "Bash|Edit|Write|MultiEdit|NotebookEdit|apply_patch|ApplyPatch|functions.apply_patch",
129
+ hooks: [{ ...handler, statusMessage: "Checking orchestrator routing" }],
130
+ },
131
+ ];
132
+ const updated = `${JSON.stringify(root, null, 2)}\n`;
133
+ const original = existsSync(target) ? readText(target) : "";
134
+ if (updated !== original) {
135
+ backup(target);
136
+ write(target, updated);
137
+ console.log(`updated: ${target}`);
138
+ }
139
+ return { target, ...commands, created };
140
+ }
141
+ function uninstallRoutingHooks(state) {
142
+ if (!existsSync(state.target)) {
143
+ console.warn(`preserved missing hooks file: ${state.target}`);
144
+ return;
145
+ }
146
+ const root = readJsonObject(state.target);
147
+ removeHookHandlers(root, state);
148
+ const hooks = root.hooks;
149
+ if (isRecord(hooks) && !Object.keys(hooks).length)
150
+ delete root.hooks;
151
+ backup(state.target);
152
+ if (state.created && !Object.keys(root).length)
153
+ rmSync(state.target);
154
+ else
155
+ write(state.target, `${JSON.stringify(root, null, 2)}\n`);
156
+ console.log(`removed managed routing hooks from: ${state.target}`);
157
+ }
47
158
  function loadState(home) {
48
159
  const file = join(home, STATE_FILE);
49
160
  if (!existsSync(file))
@@ -234,6 +345,7 @@ function installGlobal(options) {
234
345
  version: PACKAGE.version,
235
346
  files: {},
236
347
  globalAgents: null,
348
+ hooks: null,
237
349
  config: prior.config ?? null,
238
350
  };
239
351
  for (const name of readdirSync(AGENTS_DIR).filter((name) => name.endsWith(".toml")).sort()) {
@@ -245,6 +357,9 @@ function installGlobal(options) {
245
357
  const routingRecord = installFile(ROUTING_FILE, join(home, "SUBAGENT_ROUTING.md"), "routing", prior, options.force);
246
358
  if (routingRecord)
247
359
  next.files.routing = routingRecord;
360
+ const hookRecord = installFile(ROUTING_HOOK_FILE, join(home, "codex-kit", "routing-hook.js"), "routing-hook", prior, options.force);
361
+ if (hookRecord)
362
+ next.files["routing-hook"] = hookRecord;
248
363
  for (const [key, record] of Object.entries(prior.files)) {
249
364
  if (key in next.files)
250
365
  continue;
@@ -272,6 +387,7 @@ function installGlobal(options) {
272
387
  console.log(`updated: ${globalAgents}`);
273
388
  }
274
389
  next.globalAgents = { target: globalAgents };
390
+ next.hooks = installRoutingHooks(home, prior.hooks);
275
391
  saveState(home, next);
276
392
  const commitPusher = join(home, "agents", "commit-pusher.toml");
277
393
  if (existsSync(commitPusher)) {
@@ -360,6 +476,11 @@ function listGlobal(options) {
360
476
  const hasRoutingBlock = existsSync(globalAgents) && readText(globalAgents).includes(GLOBAL_BEGIN);
361
477
  console.log(`Global routing: ${hasRoutingBlock ? "installed" : "not installed"}`);
362
478
  console.log(`Routing file: ${existsSync(routingFile) ? routingFile : "missing"}`);
479
+ const routingHook = state.hooks;
480
+ const hooksInstalled = Boolean(routingHook &&
481
+ existsSync(routingHook.target) &&
482
+ readText(routingHook.target).includes(routingHook.command));
483
+ console.log(`Routing hook: ${hooksInstalled ? "installed" : "not installed"}`);
363
484
  console.log("Custom agents:");
364
485
  const agents = existsSync(agentsDir)
365
486
  ? readdirSync(agentsDir).filter((name) => name.endsWith(".toml")).sort()
@@ -441,6 +562,14 @@ function uninstallGlobal(options) {
441
562
  }
442
563
  }
443
564
  }
565
+ if (state.hooks)
566
+ uninstallRoutingHooks(state.hooks);
567
+ const allowancesDir = join(home, "codex-kit", "allowances");
568
+ if (existsSync(allowancesDir))
569
+ rmSync(allowancesDir, { recursive: true, force: true });
570
+ const kitDir = join(home, "codex-kit");
571
+ if (existsSync(kitDir) && !readdirSync(kitDir).length)
572
+ rmSync(kitDir, { recursive: true });
444
573
  rmSync(statePath);
445
574
  console.log(`Codex kit uninstalled from ${home}`);
446
575
  }
@@ -0,0 +1,166 @@
1
+ #!/usr/bin/env node
2
+ import { createHash } from "node:crypto";
3
+ import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
4
+ import { dirname, join, resolve } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ const home = resolve(dirname(fileURLToPath(import.meta.url)), "..");
7
+ const routingFile = join(home, "SUBAGENT_ROUTING.md");
8
+ const allowancesDir = join(home, "codex-kit", "allowances");
9
+ const allowanceTtlMs = 15 * 60 * 1000;
10
+ const writeTools = new Set([
11
+ "Edit",
12
+ "Write",
13
+ "MultiEdit",
14
+ "NotebookEdit",
15
+ "apply_patch",
16
+ "ApplyPatch",
17
+ "functions.apply_patch",
18
+ ]);
19
+ const writeCommands = new Set([
20
+ "chmod",
21
+ "chown",
22
+ "cp",
23
+ "dd",
24
+ "install",
25
+ "ln",
26
+ "mkdir",
27
+ "mv",
28
+ "rm",
29
+ "rmdir",
30
+ "rsync",
31
+ "tee",
32
+ "touch",
33
+ "truncate",
34
+ ]);
35
+ const gitWriteCommands = new Set([
36
+ "add",
37
+ "am",
38
+ "apply",
39
+ "checkout",
40
+ "cherry-pick",
41
+ "clean",
42
+ "commit",
43
+ "merge",
44
+ "mv",
45
+ "rebase",
46
+ "reset",
47
+ "restore",
48
+ "revert",
49
+ "rm",
50
+ "stash",
51
+ "switch",
52
+ ]);
53
+ const input = JSON.parse(readFileSync(0, "utf8"));
54
+ function allowanceFile(payload) {
55
+ if (!payload.session_id || !payload.turn_id)
56
+ return null;
57
+ const key = createHash("sha256")
58
+ .update(`${payload.session_id}\0${payload.turn_id}`)
59
+ .digest("hex");
60
+ return join(allowancesDir, `${key}.json`);
61
+ }
62
+ function writeAllowance(payload) {
63
+ const file = allowanceFile(payload);
64
+ if (!file)
65
+ return false;
66
+ mkdirSync(allowancesDir, { recursive: true });
67
+ const temporary = `${file}.${process.pid}.tmp`;
68
+ writeFileSync(temporary, JSON.stringify({
69
+ agentId: payload.agent_id,
70
+ agentType: payload.agent_type,
71
+ expiresAt: Date.now() + allowanceTtlMs,
72
+ }));
73
+ renameSync(temporary, file);
74
+ return true;
75
+ }
76
+ function removeAllowance(payload) {
77
+ const file = allowanceFile(payload);
78
+ if (file)
79
+ rmSync(file, { force: true });
80
+ }
81
+ function hasAllowance(payload) {
82
+ const file = allowanceFile(payload);
83
+ if (!file || !existsSync(file))
84
+ return false;
85
+ try {
86
+ const value = JSON.parse(readFileSync(file, "utf8"));
87
+ if (typeof value.expiresAt === "number" && value.expiresAt > Date.now())
88
+ return true;
89
+ }
90
+ catch {
91
+ // Invalid allowance files fail closed.
92
+ }
93
+ rmSync(file, { force: true });
94
+ return false;
95
+ }
96
+ function pruneAllowances() {
97
+ if (!existsSync(allowancesDir))
98
+ return;
99
+ for (const name of readdirSync(allowancesDir)) {
100
+ if (!name.endsWith(".json"))
101
+ continue;
102
+ const file = join(allowancesDir, name);
103
+ try {
104
+ const value = JSON.parse(readFileSync(file, "utf8"));
105
+ if (typeof value.expiresAt === "number" && value.expiresAt > Date.now())
106
+ continue;
107
+ }
108
+ catch {
109
+ // Invalid allowance files are stale.
110
+ }
111
+ rmSync(file, { force: true });
112
+ }
113
+ }
114
+ function isWriteShapedBash(command) {
115
+ if (/(^|[^<])(?:>>?|&>>?|\d>>?)\s*[^&]/.test(command))
116
+ return true;
117
+ const commands = command.split(/(?:&&|\|\||;|\|)/).map((part) => part.trim()).filter(Boolean);
118
+ return commands.some((segment) => {
119
+ const tokens = segment.match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g) ?? [];
120
+ const executable = tokens[0]?.split("/").at(-1);
121
+ if (!executable)
122
+ return false;
123
+ if (writeCommands.has(executable))
124
+ return true;
125
+ if ((executable === "sed" || executable === "perl") && tokens.slice(1, 5).some((token) => /^-[^-]*i/.test(token)))
126
+ return true;
127
+ return executable === "git" && Boolean(tokens[1] && gitWriteCommands.has(tokens[1]));
128
+ });
129
+ }
130
+ function hookContext(event, context) {
131
+ return JSON.stringify({
132
+ hookSpecificOutput: {
133
+ hookEventName: event,
134
+ additionalContext: context,
135
+ },
136
+ });
137
+ }
138
+ if (input.hook_event_name === "UserPromptSubmit" && existsSync(routingFile)) {
139
+ const routing = readFileSync(routingFile, "utf8").trim();
140
+ process.stdout.write(`Codex-kit routing policy for this turn:\n\n${routing}\n\n` +
141
+ "Classify the task using this policy before acting. When it requires delegation, " +
142
+ "spawn the exact named role before doing that role's work. Agent definitions, not " +
143
+ "this policy, determine each role's model and reasoning effort.");
144
+ }
145
+ if (input.hook_event_name === "SubagentStart") {
146
+ pruneAllowances();
147
+ const recorded = writeAllowance(input);
148
+ process.stdout.write(hookContext("SubagentStart", `You are the delegated ${input.agent_type ?? "worker"}. ` +
149
+ `${recorded ? "A temporary write lane is active." : "No write lane could be recorded."} ` +
150
+ "Follow the assigned scope, perform the role's work directly without further delegation, validate it, and return concise evidence."));
151
+ }
152
+ if (input.hook_event_name === "SubagentStop")
153
+ removeAllowance(input);
154
+ if (input.hook_event_name === "PreToolUse") {
155
+ const writeAttempt = writeTools.has(input.tool_name ?? "") ||
156
+ (input.tool_name === "Bash" && isWriteShapedBash(input.tool_input?.command ?? ""));
157
+ if (writeAttempt && !input.agent_id && !hasAllowance(input)) {
158
+ process.stdout.write(JSON.stringify({
159
+ hookSpecificOutput: {
160
+ hookEventName: "PreToolUse",
161
+ permissionDecision: "deny",
162
+ permissionDecisionReason: "The root orchestrator may not write directly. Consult SUBAGENT_ROUTING.md and delegate to the exact role selected there.",
163
+ },
164
+ }));
165
+ }
166
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iamdevlinph/codex-kit",
3
- "version": "1.0.4",
3
+ "version": "1.0.6",
4
4
  "description": "Portable Codex subagents and project AGENTS.md defaults.",
5
5
  "type": "module",
6
6
  "bin": {