approval-md 0.1.0 → 0.2.0

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.
Files changed (266) hide show
  1. package/README.md +584 -553
  2. package/SPEC.md +42 -13
  3. package/dist/src/adapters/agentmail.d.ts +426 -0
  4. package/dist/src/adapters/agentmail.js +2 -2
  5. package/dist/src/adapters/conformance.d.ts +149 -0
  6. package/dist/src/adapters/contract.d.ts +628 -0
  7. package/dist/src/adapters/contract.js +110 -16
  8. package/dist/src/adapters/contract.js.map +1 -1
  9. package/dist/src/adapters/email.d.ts +324 -0
  10. package/dist/src/adapters/env-passphrase.d.ts +93 -0
  11. package/dist/src/adapters/public.d.ts +11 -0
  12. package/dist/src/adapters/public.js +11 -0
  13. package/dist/src/adapters/public.js.map +1 -0
  14. package/dist/src/adapters/registry.d.ts +59 -0
  15. package/dist/src/adapters/registry.js +2 -1
  16. package/dist/src/adapters/registry.js.map +1 -1
  17. package/dist/src/adapters/smtp.d.ts +213 -0
  18. package/dist/src/adapters/vault-provider.d.ts +114 -0
  19. package/dist/src/adapters/vault-provider.js +3 -3
  20. package/dist/src/adapters/zzz.d.ts +66 -0
  21. package/dist/src/adapters/zzz.js +299 -0
  22. package/dist/src/adapters/zzz.js.map +1 -0
  23. package/dist/src/channels/batch.d.ts +109 -0
  24. package/dist/src/channels/cli.d.ts +193 -0
  25. package/dist/src/channels/conformance.d.ts +92 -0
  26. package/dist/src/channels/contract.d.ts +623 -0
  27. package/dist/src/channels/payload-view.d.ts +35 -0
  28. package/dist/src/channels/render-queue.d.ts +149 -0
  29. package/dist/src/channels/tagging.d.ts +196 -0
  30. package/dist/src/channels/telegram.d.ts +1832 -0
  31. package/dist/src/channels/web.d.ts +341 -0
  32. package/dist/src/cli/adapter.d.ts +90 -0
  33. package/dist/src/cli/adapter.js +25 -15
  34. package/dist/src/cli/adapter.js.map +1 -1
  35. package/dist/src/cli/amend.d.ts +59 -0
  36. package/dist/src/cli/args.d.ts +43 -0
  37. package/dist/src/cli/attest.d.ts +41 -0
  38. package/dist/src/cli/audit-card.d.ts +62 -0
  39. package/dist/src/cli/audit.d.ts +59 -0
  40. package/dist/src/cli/channel-telegram.d.ts +806 -0
  41. package/dist/src/cli/channel-web.d.ts +131 -0
  42. package/dist/src/cli/channel.d.ts +71 -0
  43. package/dist/src/cli/checkpoint-tap.d.ts +169 -0
  44. package/dist/src/cli/codex.d.ts +2 -0
  45. package/dist/src/cli/codex.js +172 -0
  46. package/dist/src/cli/codex.js.map +1 -0
  47. package/dist/src/cli/coverage.d.ts +61 -0
  48. package/dist/src/cli/daemon.d.ts +120 -0
  49. package/dist/src/cli/doctor.d.ts +129 -0
  50. package/dist/src/cli/doctor.js +119 -5
  51. package/dist/src/cli/doctor.js.map +1 -1
  52. package/dist/src/cli/env.d.ts +65 -0
  53. package/dist/src/cli/execute.d.ts +202 -0
  54. package/dist/src/cli/exit-codes.d.ts +73 -0
  55. package/dist/src/cli/feedback.d.ts +60 -0
  56. package/dist/src/cli/gate-window.d.ts +40 -0
  57. package/dist/src/cli/gate.d.ts +68 -0
  58. package/dist/src/cli/git-scope.d.ts +190 -0
  59. package/dist/src/cli/gloss-attach.d.ts +85 -0
  60. package/dist/src/cli/gloss-codex-child.d.ts +9 -0
  61. package/dist/src/cli/gloss-codex.d.ts +24 -0
  62. package/dist/src/cli/gloss-options.d.ts +42 -0
  63. package/dist/src/cli/gloss.d.ts +265 -0
  64. package/dist/src/cli/help.d.ts +103 -0
  65. package/dist/src/cli/help.js +173 -51
  66. package/dist/src/cli/help.js.map +1 -1
  67. package/dist/src/cli/hook-codex.d.ts +78 -0
  68. package/dist/src/cli/hook-codex.js +167 -0
  69. package/dist/src/cli/hook-codex.js.map +1 -0
  70. package/dist/src/cli/hook.d.ts +331 -0
  71. package/dist/src/cli/hook.js +186 -80
  72. package/dist/src/cli/hook.js.map +1 -1
  73. package/dist/src/cli/import.d.ts +35 -0
  74. package/dist/src/cli/init.d.ts +84 -0
  75. package/dist/src/cli/init.js +2 -2
  76. package/dist/src/cli/init.js.map +1 -1
  77. package/dist/src/cli/instructions.d.ts +23 -0
  78. package/dist/src/cli/journal.d.ts +41 -0
  79. package/dist/src/cli/log-advance.d.ts +287 -0
  80. package/dist/src/cli/log-advance.js +102 -11
  81. package/dist/src/cli/log-advance.js.map +1 -1
  82. package/dist/src/cli/log-anchor.d.ts +176 -0
  83. package/dist/src/cli/log-checkpoint.d.ts +22 -0
  84. package/dist/src/cli/log-sync.d.ts +243 -0
  85. package/dist/src/cli/log-verbs.d.ts +16 -0
  86. package/dist/src/cli/log-verbs.js +7 -1
  87. package/dist/src/cli/log-verbs.js.map +1 -1
  88. package/dist/src/cli/long-help.d.ts +70 -0
  89. package/dist/src/cli/main.d.ts +77 -0
  90. package/dist/src/cli/main.js +155 -5
  91. package/dist/src/cli/main.js.map +1 -1
  92. package/dist/src/cli/mcp.d.ts +52 -0
  93. package/dist/src/cli/paths.d.ts +56 -0
  94. package/dist/src/cli/payload.d.ts +58 -0
  95. package/dist/src/cli/policy.d.ts +43 -0
  96. package/dist/src/cli/preflight.d.ts +363 -0
  97. package/dist/src/cli/preflight.js +294 -7
  98. package/dist/src/cli/preflight.js.map +1 -1
  99. package/dist/src/cli/progress.d.ts +78 -0
  100. package/dist/src/cli/prompt.d.ts +209 -0
  101. package/dist/src/cli/quickstart.d.ts +46 -0
  102. package/dist/src/cli/quickstart.js +297 -0
  103. package/dist/src/cli/quickstart.js.map +1 -0
  104. package/dist/src/cli/records.d.ts +34 -0
  105. package/dist/src/cli/render.d.ts +22 -0
  106. package/dist/src/cli/sandbox.d.ts +51 -0
  107. package/dist/src/cli/scaffold.d.ts +79 -0
  108. package/dist/src/cli/setup-adapter.d.ts +137 -0
  109. package/dist/src/cli/setup-adapter.js +38 -4
  110. package/dist/src/cli/setup-adapter.js.map +1 -1
  111. package/dist/src/cli/setup-channel.d.ts +117 -0
  112. package/dist/src/cli/setup-checkpoint.d.ts +57 -0
  113. package/dist/src/cli/setup-common.d.ts +275 -0
  114. package/dist/src/cli/setup-flow.d.ts +287 -0
  115. package/dist/src/cli/setup-service.d.ts +96 -0
  116. package/dist/src/cli/setup.d.ts +202 -0
  117. package/dist/src/cli/style.d.ts +320 -0
  118. package/dist/src/cli/token.d.ts +39 -0
  119. package/dist/src/cli/up.d.ts +155 -0
  120. package/dist/src/cli/up.js +4 -2
  121. package/dist/src/cli/up.js.map +1 -1
  122. package/dist/src/cli/usage.d.ts +37 -0
  123. package/dist/src/cli/values.d.ts +40 -0
  124. package/dist/src/cli/vault.d.ts +59 -0
  125. package/dist/src/cli/vault.js +2 -2
  126. package/dist/src/cli/vault.js.map +1 -1
  127. package/dist/src/cli/verb-registry.d.ts +76 -0
  128. package/dist/src/cli/verb-registry.js +176 -8
  129. package/dist/src/cli/verb-registry.js.map +1 -1
  130. package/dist/src/cli/wordmark.d.ts +31 -0
  131. package/dist/src/cli/wordmark.js +2 -2
  132. package/dist/src/codex/doctor.d.ts +13 -0
  133. package/dist/src/codex/doctor.js +41 -0
  134. package/dist/src/codex/doctor.js.map +1 -0
  135. package/dist/src/codex/manifest.d.ts +49 -0
  136. package/dist/src/codex/manifest.js +103 -0
  137. package/dist/src/codex/manifest.js.map +1 -0
  138. package/dist/src/codex/templates.d.ts +41 -0
  139. package/dist/src/codex/templates.js +319 -0
  140. package/dist/src/codex/templates.js.map +1 -0
  141. package/dist/src/codex/trust.d.ts +19 -0
  142. package/dist/src/codex/trust.js +183 -0
  143. package/dist/src/codex/trust.js.map +1 -0
  144. package/dist/src/codex/workspace-plan.d.ts +131 -0
  145. package/dist/src/codex/workspace-plan.js +561 -0
  146. package/dist/src/codex/workspace-plan.js.map +1 -0
  147. package/dist/src/core/actor.d.ts +2 -0
  148. package/dist/src/core/actor.js +5 -0
  149. package/dist/src/core/actor.js.map +1 -0
  150. package/dist/src/core/advance-cycle.d.ts +170 -0
  151. package/dist/src/core/agents-md.d.ts +276 -0
  152. package/dist/src/core/apply-patch.d.ts +49 -0
  153. package/dist/src/core/apply-patch.js +266 -0
  154. package/dist/src/core/apply-patch.js.map +1 -0
  155. package/dist/src/core/attest.d.ts +420 -0
  156. package/dist/src/core/attest.js +13 -1
  157. package/dist/src/core/attest.js.map +1 -1
  158. package/dist/src/core/audit.d.ts +492 -0
  159. package/dist/src/core/budgets.d.ts +238 -0
  160. package/dist/src/core/checkpoint.d.ts +500 -0
  161. package/dist/src/core/child-env.d.ts +88 -0
  162. package/dist/src/core/clock.d.ts +52 -0
  163. package/dist/src/core/command-class.d.ts +543 -0
  164. package/dist/src/core/command-class.js +43 -8
  165. package/dist/src/core/command-class.js.map +1 -1
  166. package/dist/src/core/coverage-sources/adapter.d.ts +40 -0
  167. package/dist/src/core/coverage-sources/gh.d.ts +48 -0
  168. package/dist/src/core/coverage-sources/git.d.ts +101 -0
  169. package/dist/src/core/coverage.d.ts +217 -0
  170. package/dist/src/core/credential-spec.d.ts +72 -0
  171. package/dist/src/core/dark-session.d.ts +331 -0
  172. package/dist/src/core/decision-refusal.d.ts +185 -0
  173. package/dist/src/core/env-file.d.ts +450 -0
  174. package/dist/src/core/execute.d.ts +858 -0
  175. package/dist/src/core/execute.js +44 -6
  176. package/dist/src/core/execute.js.map +1 -1
  177. package/dist/src/core/frontmatter.d.ts +78 -0
  178. package/dist/src/core/gate-window.d.ts +312 -0
  179. package/dist/src/core/gate.d.ts +1364 -0
  180. package/dist/src/core/gate.js +68 -13
  181. package/dist/src/core/gate.js.map +1 -1
  182. package/dist/src/core/git-run.d.ts +73 -0
  183. package/dist/src/core/harness-version.d.ts +157 -0
  184. package/dist/src/core/harness-version.js +2 -1
  185. package/dist/src/core/harness-version.js.map +1 -1
  186. package/dist/src/core/harness-wait.d.ts +55 -0
  187. package/dist/src/core/head-retry.d.ts +107 -0
  188. package/dist/src/core/instance.d.ts +253 -0
  189. package/dist/src/core/intake-limits.d.ts +247 -0
  190. package/dist/src/core/jcs.d.ts +52 -0
  191. package/dist/src/core/journal.d.ts +144 -0
  192. package/dist/src/core/live-draw.d.ts +436 -0
  193. package/dist/src/core/log-reconcile.d.ts +89 -0
  194. package/dist/src/core/log-subscribe.d.ts +36 -0
  195. package/dist/src/core/log-subscribe.js +162 -0
  196. package/dist/src/core/log-subscribe.js.map +1 -0
  197. package/dist/src/core/log.d.ts +278 -0
  198. package/dist/src/core/loop.d.ts +274 -0
  199. package/dist/src/core/loop.js +11 -0
  200. package/dist/src/core/loop.js.map +1 -1
  201. package/dist/src/core/md-fence.d.ts +41 -0
  202. package/dist/src/core/money.d.ts +147 -0
  203. package/dist/src/core/payload-census.d.ts +74 -0
  204. package/dist/src/core/payload-store.d.ts +175 -0
  205. package/dist/src/core/payload.d.ts +71 -0
  206. package/dist/src/core/policy-diff.d.ts +292 -0
  207. package/dist/src/core/policy-diff.js +27 -4
  208. package/dist/src/core/policy-diff.js.map +1 -1
  209. package/dist/src/core/policy-expectations.d.ts +199 -0
  210. package/dist/src/core/policy-explain.d.ts +150 -0
  211. package/dist/src/core/policy-explain.js +31 -3
  212. package/dist/src/core/policy-explain.js.map +1 -1
  213. package/dist/src/core/policy-load.d.ts +527 -0
  214. package/dist/src/core/policy-load.js +15 -3
  215. package/dist/src/core/policy-load.js.map +1 -1
  216. package/dist/src/core/policy-match.d.ts +281 -0
  217. package/dist/src/core/policy-match.js +20 -9
  218. package/dist/src/core/policy-match.js.map +1 -1
  219. package/dist/src/core/policy-proposal.d.ts +265 -0
  220. package/dist/src/core/prompt-layout.d.ts +221 -0
  221. package/dist/src/core/protected-path-guard.d.ts +453 -0
  222. package/dist/src/core/protected-path-guard.js +514 -35
  223. package/dist/src/core/protected-path-guard.js.map +1 -1
  224. package/dist/src/core/registration.d.ts +25 -0
  225. package/dist/src/core/reindex.d.ts +99 -0
  226. package/dist/src/core/sampler.d.ts +313 -0
  227. package/dist/src/core/sandbox.d.ts +290 -0
  228. package/dist/src/core/seal.d.ts +165 -0
  229. package/dist/src/core/state.d.ts +505 -0
  230. package/dist/src/core/task-file.d.ts +185 -0
  231. package/dist/src/core/telegram-config.d.ts +93 -0
  232. package/dist/src/core/token.d.ts +409 -0
  233. package/dist/src/core/token.js +21 -38
  234. package/dist/src/core/token.js.map +1 -1
  235. package/dist/src/core/validate.d.ts +138 -0
  236. package/dist/src/core/values.d.ts +137 -0
  237. package/dist/src/core/vault.d.ts +291 -0
  238. package/dist/src/core/verified-snapshot.d.ts +204 -0
  239. package/dist/src/core/verify.d.ts +336 -0
  240. package/dist/src/core/version.d.ts +8 -0
  241. package/dist/src/core/wysiwys.d.ts +370 -0
  242. package/dist/src/daemon/advance-child.d.ts +39 -0
  243. package/dist/src/daemon/advance.d.ts +466 -0
  244. package/dist/src/daemon/audit.d.ts +87 -0
  245. package/dist/src/daemon/daemon.d.ts +1180 -0
  246. package/dist/src/daemon/dark-session.d.ts +64 -0
  247. package/dist/src/daemon/draw-child.d.ts +36 -0
  248. package/dist/src/daemon/draw.d.ts +154 -0
  249. package/dist/src/daemon/git-evidence.d.ts +173 -0
  250. package/dist/src/daemon/git-evidence.js +1 -1
  251. package/dist/src/daemon/projection.d.ts +180 -0
  252. package/dist/src/daemon/prune.d.ts +207 -0
  253. package/dist/src/mcp/http.d.ts +113 -0
  254. package/dist/src/mcp/server.d.ts +265 -0
  255. package/dist/src/mcp/server.js +9 -1
  256. package/dist/src/mcp/server.js.map +1 -1
  257. package/docs/adapter-api.md +106 -0
  258. package/docs/cli-reference.md +389 -36
  259. package/docs/codex-enforced-session.md +30 -0
  260. package/package.json +12 -2
  261. package/schema/codex-instance.schema.json +82 -0
  262. package/schema/event.schema.json +2 -1
  263. package/schema/fixtures/codex-instance/invalid/unpinned-codex-version.json +40 -0
  264. package/schema/fixtures/codex-instance/valid/canonical.json +40 -0
  265. package/schema/policy.schema.json +21 -1
  266. package/templates/codex/README.md +9 -0
@@ -86,6 +86,7 @@ import { abandonedAfterMs, HOOK_DEFAULT_WAIT, HOOK_RETRY_GRACE_MS, } from "../co
86
86
  import { harnessLoopFloor, isLoopEscalated, isSideEffectingClass, loopClearance, UNKNOWN_SESSION, } from "../core/loop.js";
87
87
  import { drawSocketPathFor, drawSocketUsable } from "../core/live-draw.js";
88
88
  import { payloadHash } from "../core/payload.js";
89
+ import { classifyApplyPatch, parseApplyPatch } from "../core/apply-patch.js";
89
90
  import { loadPolicy, parseDuration } from "../core/policy-load.js";
90
91
  import { humanOnlyRefusal, resolve as resolvePolicy } from "../core/policy-match.js";
91
92
  import { payloadOf, readVerifiedRecords, requestState, useVerifiedSnapshots, } from "../core/state.js";
@@ -96,6 +97,7 @@ import { HOOK_HELP } from "./help.js";
96
97
  import { DEFAULT_LOG_PATH } from "./paths.js";
97
98
  import { refusal as renderRefusal, style, table } from "./style.js";
98
99
  import { usageErrorText } from "./usage.js";
100
+ import { checkCodexHookInput, codexBinding, CODEX_POST_TOOL_EVENT, readCodexReportedOutcome, } from "./hook-codex.js";
99
101
  /** Identity accepted for the proposing side: a person or an agent. */
100
102
  const PRINCIPAL_ACTOR = /^(human|agent):.+/u;
101
103
  /**
@@ -290,6 +292,14 @@ const CURSOR_ADAPTER = {
290
292
  shellTool: "Shell",
291
293
  fileTools: ["Write", "Delete"],
292
294
  };
295
+ const CODEX_ADAPTER = {
296
+ kind: "codex",
297
+ originApp: "codex-hook",
298
+ defaultActor: "agent:codex",
299
+ shellTool: "Bash",
300
+ fileTools: ["apply_patch"],
301
+ bindToolName: true,
302
+ };
293
303
  /**
294
304
  * The decision object the harness reads from stdout.
295
305
  *
@@ -297,7 +307,7 @@ const CURSOR_ADAPTER = {
297
307
  * `{permission, user_message, agent_message}`. One construction site per
298
308
  * harness, still never `ask`.
299
309
  */
300
- function decision(permission, reason, harness) {
310
+ function decision(permission, reason, harness, codexCommand) {
301
311
  if (harness === "cursor") {
302
312
  return `${JSON.stringify({
303
313
  permission,
@@ -305,16 +315,22 @@ function decision(permission, reason, harness) {
305
315
  agent_message: reason,
306
316
  })}\n`;
307
317
  }
308
- return `${JSON.stringify({
309
- hookSpecificOutput: {
310
- hookEventName: "PreToolUse",
311
- permissionDecision: permission,
312
- permissionDecisionReason: reason,
313
- },
314
- })}\n`;
318
+ const hookSpecificOutput = {
319
+ hookEventName: "PreToolUse",
320
+ permissionDecision: permission,
321
+ permissionDecisionReason: reason,
322
+ };
323
+ if (harness === "codex" && permission === "allow") {
324
+ if (codexCommand !== undefined)
325
+ hookSpecificOutput["updatedInput"] = { command: codexCommand };
326
+ }
327
+ return `${JSON.stringify({ hookSpecificOutput })}\n`;
315
328
  }
316
- function allow(streams, reason, harness) {
317
- streams.out(decision("allow", reason, harness));
329
+ function allow(streams, reason, harness, codexCommand) {
330
+ if (harness === "codex" && codexCommand === undefined) {
331
+ return deny(streams, "hook-io", "the Codex allow lost its exact bound tool_input.command", harness);
332
+ }
333
+ streams.out(decision("allow", reason, harness, codexCommand));
318
334
  return EXIT_OK;
319
335
  }
320
336
  function deny(streams, code, detail, harness) {
@@ -359,13 +375,15 @@ function parseHookInput(raw) {
359
375
  ? toolInputValue
360
376
  : {};
361
377
  const responseValue = fields["tool_response"];
378
+ const sessionId = readString(fields, "session_id");
362
379
  return {
363
380
  ok: true,
364
381
  input: {
365
382
  // The ONE shared bucket for an unreadable session (`core/loop.ts`'s
366
383
  // `UNKNOWN_SESSION`): absence accrues faster than a readable id and never
367
384
  // slower, which is the fail-closed direction.
368
- sessionId: readString(fields, "session_id") ?? UNKNOWN_SESSION,
385
+ sessionId: sessionId ?? UNKNOWN_SESSION,
386
+ sessionIdPresent: sessionId !== null,
369
387
  cwd: readString(fields, "cwd") ?? "",
370
388
  toolName,
371
389
  toolInput,
@@ -376,6 +394,7 @@ function parseHookInput(raw) {
376
394
  toolResponse: typeof responseValue === "object" && responseValue !== null && !Array.isArray(responseValue)
377
395
  ? responseValue
378
396
  : null,
397
+ toolResponseRaw: responseValue,
379
398
  },
380
399
  };
381
400
  }
@@ -1037,13 +1056,12 @@ const WORKSPACE_WRITE_CLASS = "files.write.workspace";
1037
1056
  /**
1038
1057
  * What a non-Bash tool call asks for, or `null` when it names no file at all.
1039
1058
  *
1040
- * Only one thing about a file edit is a gate question at v0.1: whether the file
1041
- * is one only a human may write. Everything else the harness edits is
1042
- * `files.write.workspace`, which this repository's policy makes autonomous, and
1043
- * routing every keystroke of ordinary editing through a gate check would spend
1044
- * latency to reach a foregone conclusion.
1059
+ * Every file edit is a gate question. Protected targets take their derived
1060
+ * protected class; every other target is `files.write.workspace`. The policy
1061
+ * decides the autonomy of either class, so changing the ordinary-file rule to
1062
+ * manual, supervised or human-only changes the hook verdict too (APRV-304).
1045
1063
  *
1046
- * ## The ordinary edit still gets a class (APRV-303)
1064
+ * ## The ordinary edit gets a class and follows it (APRV-303, APRV-304)
1047
1065
  *
1048
1066
  * It used to get none: an unprotected target returned `null` here, and
1049
1067
  * `describeToolCall` answered `allow` from a branch that sits ABOVE the loop
@@ -1053,12 +1071,10 @@ const WORKSPACE_WRITE_CLASS = "files.write.workspace";
1053
1071
  * eight edits to the same file, under a standing floor, none of them routed and
1054
1072
  * none of them counted.
1055
1073
  *
1056
- * The foregone conclusion is still foregone, and is still answered without
1057
- * asking anybody: `describeToolCall` marks the call {@link FileGate.protectedPath}
1058
- * `false`, and `runHarnessHook` allows it outright the moment it establishes
1059
- * that no floor is standing. What it can no longer do is skip that
1060
- * establishment. The floor predicate is now one predicate over one class for
1061
- * every tool kind, which is what amended SPEC.md §10.2 asks for.
1074
+ * APRV-303 made the floor predicate one predicate over one class for every tool
1075
+ * kind. APRV-304 carries the same class through the rest of the shared path:
1076
+ * policy resolution, human-only refusal, budgets, registration and execution
1077
+ * accounting. An open window records the bypass before allowing the edit.
1062
1078
  *
1063
1079
  * ## The payload is the change (APRV-124)
1064
1080
  *
@@ -1639,7 +1655,7 @@ floor = null) {
1639
1655
  const floorApplies = (cls) => floor !== null && isSideEffectingClass(cls);
1640
1656
  const hash = payloadHash(payload);
1641
1657
  const summary = truncate(headline, SUMMARY_LIMIT);
1642
- const sayAllow = (reason) => allow(streams, reason, run.harness);
1658
+ const sayAllow = (reason) => allow(streams, reason, run.harness, run.codexCommand);
1643
1659
  /**
1644
1660
  * Every deny this function can print, with the floor's own sentence appended
1645
1661
  * when a floor is what routed the command here (APRV-280). One wrapper rather
@@ -2077,7 +2093,9 @@ function report(streams, code, detail, extra = {}) {
2077
2093
  * whether two enumerated fields are present and what kind of value they hold.
2078
2094
  * No text from any of them reaches the log or this function's return.
2079
2095
  */
2080
- function readReportedOutcome(input) {
2096
+ function readReportedOutcome(input, adapter) {
2097
+ if (adapter.kind === "codex")
2098
+ return readCodexReportedOutcome(input);
2081
2099
  const event = input.hookEventName;
2082
2100
  if (event !== "PostToolUse" && event !== "PostToolUseFailure") {
2083
2101
  return {
@@ -2124,7 +2142,7 @@ function runPostToolUse(flags, streams, cwd, input, actor, adapter) {
2124
2142
  // `approval status` rather than closed against a guess.
2125
2143
  return report(streams, "post-tool-unidentified", "the event carries no tool_use_id, so the task id the pre-execution hook minted cannot be reconstructed; nothing was appended");
2126
2144
  }
2127
- const reading = readReportedOutcome(input);
2145
+ const reading = readReportedOutcome(input, adapter);
2128
2146
  if (!reading.ok) {
2129
2147
  return report(streams, "post-tool-unreadable-outcome", `${reading.detail}; nothing was appended`);
2130
2148
  }
@@ -2133,8 +2151,8 @@ function runPostToolUse(flags, streams, cwd, input, actor, adapter) {
2133
2151
  return report(streams, "post-tool-log-unreachable", `no log at ${logPath}; the hook writes to an existing log and never creates one. Run \`approval init\` in ${root}`);
2134
2152
  }
2135
2153
  const finished = finishHarnessExecution(logPath, {
2136
- sessionId: input.sessionId,
2137
- toolUseId: input.toolUseId,
2154
+ sessionId: adapter.kind === "codex" ? codexBinding(input, cwd).finishSessionId : input.sessionId,
2155
+ toolUseId: adapter.kind === "codex" ? codexBinding(input, cwd).finishToolUseId : input.toolUseId,
2138
2156
  outcome: reading.outcome,
2139
2157
  // The one member of the closed set at v0.1. It names the untrusted
2140
2158
  // reporter and reduces nothing.
@@ -2146,6 +2164,25 @@ function runPostToolUse(flags, streams, cwd, input, actor, adapter) {
2146
2164
  return report(streams, POST_TOOL_REPORTED, `recorded ${reading.outcome} for ${String(finished.records.length)} delegated execution(s) of ${finished.task}`, { task: finished.task, outcome: reading.outcome, appended: finished.records.length });
2147
2165
  }
2148
2166
  function describeToolCall(input, adapter, protectedPaths, cwd) {
2167
+ if (adapter.kind === "codex" && input.toolName === "apply_patch") {
2168
+ const raw = readString(input.toolInput, "command");
2169
+ if (raw === null) {
2170
+ return { kind: "deny", code: "hook-io", detail: "apply_patch tool_input carries no command string" };
2171
+ }
2172
+ const parsed = parseApplyPatch(raw);
2173
+ if (!parsed.ok)
2174
+ return { kind: "deny", code: "hook-io", detail: parsed.detail };
2175
+ const classified = classifyApplyPatch(parsed, cwd, protectedPaths);
2176
+ if (!classified.ok)
2177
+ return { kind: "deny", code: "hook-io", detail: classified.detail };
2178
+ return {
2179
+ kind: "gated",
2180
+ classes: classified.classes,
2181
+ payload: codexBinding(input, cwd).payload,
2182
+ headline: `apply_patch ${classified.operations.length} operation(s)`,
2183
+ notes: classified.targets.map((target) => `${target.role} ${target.path} (${target.classes.join(", ")})`),
2184
+ };
2185
+ }
2149
2186
  if (input.toolName === adapter.shellTool) {
2150
2187
  const raw = readString(input.toolInput, "command");
2151
2188
  if (raw === null) {
@@ -2158,7 +2195,9 @@ function describeToolCall(input, adapter, protectedPaths, cwd) {
2158
2195
  // Unchanged since APRV-117, deliberately: the payload is the WHOLE command
2159
2196
  // and the directory it runs in, so the FULL PAYLOAD block on the phone
2160
2197
  // carries every byte the harness will execute. Only `summary` is shortened.
2161
- const payload = { command: raw, cwd: input.cwd };
2198
+ const payload = adapter.bindToolName
2199
+ ? codexBinding(input, cwd).payload
2200
+ : { command: raw, cwd: input.cwd };
2162
2201
  // APRV-108: a local rewrite of history this checkout never published is a
2163
2202
  // commit. APRV-267: a delete confined to the agent's own scratch is not a
2164
2203
  // decision. Both run in the hook's own cwd, after classification and never
@@ -2172,9 +2211,82 @@ function describeToolCall(input, adapter, protectedPaths, cwd) {
2172
2211
  detail: `${classified.detail} (segment: ${classified.segment}). Rewrite it as a command the classifier can read, or run the effect through \`approval run\` with a granted token.`,
2173
2212
  };
2174
2213
  }
2214
+ const classes = classified.classes.filter((cls) => cls !== GATE_SELF_CLASS);
2215
+ if (adapter.kind === "codex") {
2216
+ // The pure shell classifier sees each segment independently. Preserve
2217
+ // Codex hook organs when an earlier simple `cd` changes the directory or
2218
+ // when the hook itself runs inside an organ directory by resolving every
2219
+ // later side-effecting segment's words from the effective directory.
2220
+ const possibleCwds = new Set([cwd]);
2221
+ for (const segment of classified.segments) {
2222
+ const parsedWords = commandSegmentWords(segment.text)?.[0];
2223
+ if (parsedWords?.bin === "cd" && parsedWords.args.length !== 1) {
2224
+ return {
2225
+ kind: "deny",
2226
+ code: "hook-io",
2227
+ detail: "Codex Bash cwd changes must use exact `cd <directory>` with no additional words",
2228
+ };
2229
+ }
2230
+ if (parsedWords?.bin === "cd" &&
2231
+ (parsedWords.args[0] === "-" ||
2232
+ (!isAbsolute(parsedWords.args[0] ?? "") &&
2233
+ parsedWords.args[0] !== "." &&
2234
+ parsedWords.args[0] !== ".." &&
2235
+ !(parsedWords.args[0] ?? "").startsWith("./") &&
2236
+ !(parsedWords.args[0] ?? "").startsWith("../")))) {
2237
+ return {
2238
+ kind: "deny",
2239
+ code: "hook-io",
2240
+ detail: "Codex Bash cwd changes must name an absolute path, `.`, `..`, `./...`, or `../...`; OLDPWD and CDPATH-dependent operands are unsupported",
2241
+ };
2242
+ }
2243
+ if (isSideEffectingClass(segment.class)) {
2244
+ for (const possibleCwd of possibleCwds) {
2245
+ const cwdSegments = possibleCwd.split(/[/\\]+/u);
2246
+ const cwdClass = cwdSegments.includes(".codex")
2247
+ ? "policy.core"
2248
+ : protectedPathClass(possibleCwd, protectedPaths);
2249
+ if (cwdClass !== null && !classes.includes(cwdClass))
2250
+ classes.push(cwdClass);
2251
+ for (const word of parsedWords === undefined ? [] : [parsedWords.bin, ...parsedWords.args]) {
2252
+ const cls = protectedPathClass(resolvePathSegments(possibleCwd, word), protectedPaths);
2253
+ if (cls !== null && !classes.includes(cls))
2254
+ classes.push(cls);
2255
+ }
2256
+ }
2257
+ }
2258
+ if (parsedWords?.bin === "cd" && parsedWords.args.length === 1) {
2259
+ // Lists and conditionals may skip a cd. Retain every prior directory
2260
+ // and add each directory the cd could establish; later writes are
2261
+ // checked against their union.
2262
+ const priorCwds = Array.from(possibleCwds);
2263
+ for (const possibleCwd of priorCwds) {
2264
+ const lexical = resolvePathSegments(possibleCwd, parsedWords.args[0] ?? "");
2265
+ possibleCwds.add(lexical);
2266
+ try {
2267
+ possibleCwds.add(realpathSync(lexical));
2268
+ }
2269
+ catch {
2270
+ return {
2271
+ kind: "deny",
2272
+ code: "hook-io",
2273
+ detail: `Codex Bash cd target ${JSON.stringify(parsedWords.args[0])} could not be resolved`,
2274
+ };
2275
+ }
2276
+ if (possibleCwds.size > 64) {
2277
+ return {
2278
+ kind: "deny",
2279
+ code: "hook-io",
2280
+ detail: "Codex Bash command has more than 64 possible working directories",
2281
+ };
2282
+ }
2283
+ }
2284
+ }
2285
+ }
2286
+ }
2175
2287
  return {
2176
2288
  kind: "gated",
2177
- classes: classified.classes.filter((cls) => cls !== GATE_SELF_CLASS),
2289
+ classes,
2178
2290
  payload,
2179
2291
  headline: raw,
2180
2292
  notes: refined.notes,
@@ -2185,16 +2297,6 @@ function describeToolCall(input, adapter, protectedPaths, cwd) {
2185
2297
  if (gated === null) {
2186
2298
  return { kind: "allow", reason: `${input.toolName} names no file, so there is nothing to gate` };
2187
2299
  }
2188
- if (!gated.protectedPath) {
2189
- return {
2190
- kind: "gated",
2191
- classes: [gated.cls],
2192
- payload: gated.payload,
2193
- headline: gated.summary,
2194
- notes: [],
2195
- passthrough: `${input.toolName} is not a gated edit`,
2196
- };
2197
- }
2198
2300
  return {
2199
2301
  kind: "gated",
2200
2302
  classes: [gated.cls],
@@ -2202,7 +2304,7 @@ function describeToolCall(input, adapter, protectedPaths, cwd) {
2202
2304
  headline: gated.summary,
2203
2305
  // The tier rides in the verdict's note as well as in the payload, so an
2204
2306
  // `allow` says which checkout it authorized (APRV-124).
2205
- notes: [fileTierNote(gated)],
2307
+ notes: gated.protectedPath ? [fileTierNote(gated)] : [],
2206
2308
  };
2207
2309
  }
2208
2310
  /** The environment variable that turns the sandbox requirement on (APRV-193). */
@@ -2328,6 +2430,7 @@ function runBypass(streams, input, adapter, cwd, logPath, flags, actor, window,
2328
2430
  * shape.
2329
2431
  */
2330
2432
  decidedOn) {
2433
+ const codexCommand = adapter.kind === "codex" ? codexBinding(input, cwd).payload.command : undefined;
2331
2434
  const scope = hookScope(flags, cwd);
2332
2435
  const load = loadPolicy(scope.options.policy?.file === undefined
2333
2436
  ? { dir: scope.options.policy?.dir ?? cwd }
@@ -2340,16 +2443,8 @@ decidedOn) {
2340
2443
  if (described.kind === "deny") {
2341
2444
  return deny(streams, described.code, `${described.detail} The open window does not reach this: a command the classifier cannot read is a command nothing here can establish is safe to run unapproved.`, adapter.kind);
2342
2445
  }
2343
- if (described.kind === "allow")
2344
- return allow(streams, described.reason, adapter.kind);
2345
- if (described.passthrough !== undefined) {
2346
- // APRV-303. An ordinary workspace edit is allowed by the policy on its own
2347
- // merits, so there is nothing here for the window to suspend and nothing
2348
- // for a `gate.bypassed` record to say. The only thing that would have made
2349
- // this call a question is a §10.2 floor, and a window bypasses the floor
2350
- // outright. Answered here rather than below so the bypass log stays a
2351
- // record of calls the window actually let through.
2352
- return allow(streams, described.passthrough, adapter.kind);
2446
+ if (described.kind === "allow") {
2447
+ return allow(streams, described.reason, adapter.kind, codexCommand);
2353
2448
  }
2354
2449
  const classes = described.classes;
2355
2450
  if (classes.length === 0) {
@@ -2357,7 +2452,7 @@ decidedOn) {
2357
2452
  // record for the reason it is allowed outside a window: gating the gate
2358
2453
  // with the gate recurses, and a window that recorded its own closing verb
2359
2454
  // would be recording the act that ends it.
2360
- return allow(streams, "the approval CLI is the gate itself and is not gated by it", adapter.kind);
2455
+ return allow(streams, "the approval CLI is the gate itself and is not gated by it", adapter.kind, codexCommand);
2361
2456
  }
2362
2457
  const mutation = classes.find((cls) => cls === "log.mutate");
2363
2458
  if (mutation !== undefined) {
@@ -2400,9 +2495,10 @@ decidedOn) {
2400
2495
  }
2401
2496
  streams.err(bypassBanner(window, classes, recorded.record.seq));
2402
2497
  const notes = [...described.notes, ...(policyNote === null ? [] : [policyNote])];
2403
- return allow(streams, `gate-open: ${classes.join(", ")} bypassed by the window opened at seq ${String(window.seq)} by ${window.openedBy} (expires ${window.expiresAt}); recorded as gate.bypassed seq ${String(recorded.record.seq)}${notes.length === 0 ? "" : ` (${notes.join("; ")})`}`, adapter.kind);
2498
+ return allow(streams, `gate-open: ${classes.join(", ")} bypassed by the window opened at seq ${String(window.seq)} by ${window.openedBy} (expires ${window.expiresAt}); recorded as gate.bypassed seq ${String(recorded.record.seq)}${notes.length === 0 ? "" : ` (${notes.join("; ")})`}`, adapter.kind, codexCommand);
2404
2499
  }
2405
2500
  function runHarnessHook(argv, streams, cwd, readStdin, adapter) {
2501
+ const configurationError = (message) => adapter.kind === "codex" ? deny(streams, "hook-io", message, adapter.kind) : usageError(streams, message);
2406
2502
  const parsed = parseFlags(argv, {
2407
2503
  ...COMMON_FLAGS,
2408
2504
  ...POLICY_FLAGS,
@@ -2413,29 +2509,29 @@ function runHarnessHook(argv, streams, cwd, readStdin, adapter) {
2413
2509
  "--retry-grace": "string",
2414
2510
  });
2415
2511
  if (!parsed.ok)
2416
- return usageError(streams, parsed.message);
2512
+ return configurationError(parsed.message);
2417
2513
  if (boolFlag(parsed.flags, "--help") || boolFlag(parsed.flags, "-h")) {
2418
2514
  streams.out(`${HOOK_HELP}\n`);
2419
2515
  return EXIT_OK;
2420
2516
  }
2421
2517
  const extra = parsed.positionals[0];
2422
2518
  if (extra !== undefined) {
2423
- return usageError(streams, `unexpected argument ${JSON.stringify(extra)}`);
2519
+ return configurationError(`unexpected argument ${JSON.stringify(extra)}`);
2424
2520
  }
2425
2521
  const asFlag = stringFlag(parsed.flags, "--as");
2426
2522
  const actor = asFlag ?? adapter.defaultActor;
2427
2523
  if (!PRINCIPAL_ACTOR.test(actor)) {
2428
- return usageError(streams, `--as expects agent:<id> or human:<id>, got ${JSON.stringify(asFlag)}`);
2524
+ return configurationError(`--as expects agent:<id> or human:<id>, got ${JSON.stringify(asFlag)}`);
2429
2525
  }
2430
2526
  const timeoutText = stringFlag(parsed.flags, "--timeout") ?? DEFAULT_TIMEOUT;
2431
2527
  const timeoutMs = parseDuration(timeoutText);
2432
2528
  if (timeoutMs === null) {
2433
- return usageError(streams, `--timeout expects a duration like 30s, 9m, got ${JSON.stringify(timeoutText)}`);
2529
+ return configurationError(`--timeout expects a duration like 30s, 9m, got ${JSON.stringify(timeoutText)}`);
2434
2530
  }
2435
2531
  const intervalText = stringFlag(parsed.flags, "--interval");
2436
2532
  const intervalMs = intervalText === null ? DEFAULT_INTERVAL_MS : parseDuration(intervalText);
2437
2533
  if (intervalMs === null) {
2438
- return usageError(streams, `--interval expects a duration like 500ms, 2s, got ${JSON.stringify(intervalText)}`);
2534
+ return configurationError(`--interval expects a duration like 500ms, 2s, got ${JSON.stringify(intervalText)}`);
2439
2535
  }
2440
2536
  // APRV-287. How long the question outlives the wait, for the retry that
2441
2537
  // adopts it. The duration grammar has no zero, so the shortest window is
@@ -2443,12 +2539,18 @@ function runHarnessHook(argv, streams, cwd, readStdin, adapter) {
2443
2539
  const graceText = stringFlag(parsed.flags, "--retry-grace");
2444
2540
  const graceMs = graceText === null ? HOOK_RETRY_GRACE_MS : parseDuration(graceText);
2445
2541
  if (graceMs === null) {
2446
- return usageError(streams, `--retry-grace expects a duration like 5m, 30s, 1ms, got ${JSON.stringify(graceText)}`);
2542
+ return configurationError(`--retry-grace expects a duration like 5m, 30s, 1ms, got ${JSON.stringify(graceText)}`);
2447
2543
  }
2448
2544
  const parsedInput = parseHookInput(readStdin());
2449
2545
  if (!parsedInput.ok)
2450
2546
  return deny(streams, "hook-io", parsedInput.detail, adapter.kind);
2451
2547
  const input = parsedInput.input;
2548
+ if (adapter.kind === "codex") {
2549
+ const checked = checkCodexHookInput(input, cwd);
2550
+ if (!checked.ok)
2551
+ return deny(streams, "hook-io", checked.detail, adapter.kind);
2552
+ }
2553
+ const codexCommand = adapter.kind === "codex" ? codexBinding(input, cwd).payload.command : undefined;
2452
2554
  // APRV-145: WHICH EVENT THIS IS, read first and read at all. One command is
2453
2555
  // registered for two events, and they do opposite things — one answers before
2454
2556
  // the tool runs, the other records how it went — so the dispatch is the first
@@ -2459,7 +2561,10 @@ function runHarnessHook(argv, streams, cwd, readStdin, adapter) {
2459
2561
  // harness whose event this runtime does not recognize is a harness about to
2460
2562
  // run a command, and treating an unknown name as a no-op would be an ungated
2461
2563
  // one.
2462
- if (input.hookEventName !== null && POST_TOOL_EVENTS.includes(input.hookEventName)) {
2564
+ const postToolEvent = adapter.kind === "codex"
2565
+ ? input.hookEventName === CODEX_POST_TOOL_EVENT
2566
+ : input.hookEventName !== null && POST_TOOL_EVENTS.includes(input.hookEventName);
2567
+ if (postToolEvent) {
2463
2568
  // APRV-303. `commandHarnessHook`'s catch turns a throw into a DENY, which is
2464
2569
  // the right answer for a call that has not run yet and exactly the wrong one
2465
2570
  // here: it would print a verdict object about a tool call the harness has
@@ -2473,8 +2578,17 @@ function runHarnessHook(argv, streams, cwd, readStdin, adapter) {
2473
2578
  return report(streams, "post-tool-io", `the counterpart failed: ${cause instanceof Error ? cause.message : String(cause)}; nothing was appended, so the start this event would have closed is still open`);
2474
2579
  }
2475
2580
  }
2581
+ // Codex 0.152.1 can execute Bash in a per-call working directory that is
2582
+ // absent from tool_input while both the event cwd and this hook process stay
2583
+ // at the session root (APRV-310 native v6). A decision over the visible
2584
+ // `{command, cwd}` would therefore bind different bytes from the action the
2585
+ // harness executes. Refuse before the open-window, gate-self, carry, or
2586
+ // registration paths; none of those can supply the missing directory fact.
2587
+ if (adapter.kind === "codex" && input.toolName === "Bash") {
2588
+ return deny(streams, "hook-io", "Codex Bash is disabled because the native hook contract does not expose the effective per-call working directory; no policy or open window can authorize bytes the hook cannot bind", adapter.kind);
2589
+ }
2476
2590
  if (input.toolName !== adapter.shellTool && !adapter.fileTools.includes(input.toolName)) {
2477
- return allow(streams, `${input.toolName} is not a gated tool`, adapter.kind);
2591
+ return allow(streams, `${input.toolName} is not a gated tool`, adapter.kind, codexCommand);
2478
2592
  }
2479
2593
  // APRV-188. From here on this process may resume a verified read behind the
2480
2594
  // snapshot the daemon published, instead of walking the chain from genesis:
@@ -2534,13 +2648,14 @@ function runHarnessHook(argv, streams, cwd, readStdin, adapter) {
2534
2648
  if (described.kind === "deny") {
2535
2649
  return deny(streams, described.code, described.detail, adapter.kind);
2536
2650
  }
2537
- if (described.kind === "allow")
2538
- return allow(streams, described.reason, adapter.kind);
2651
+ if (described.kind === "allow") {
2652
+ return allow(streams, described.reason, adapter.kind, codexCommand);
2653
+ }
2539
2654
  const { classes, payload, headline } = described;
2540
2655
  /** What the history-rewrite refinement did, for the decision reason. */
2541
2656
  const notes = [...described.notes];
2542
2657
  if (classes.length === 0) {
2543
- return allow(streams, "the approval CLI is the gate itself and is not gated by it", adapter.kind);
2658
+ return allow(streams, "the approval CLI is the gate itself and is not gated by it", adapter.kind, codexCommand);
2544
2659
  }
2545
2660
  // Every path from here needs the log, the fast paths included (APRV-139):
2546
2661
  // attestation and loop-escalation are facts about the log, so the
@@ -2553,7 +2668,9 @@ function runHarnessHook(argv, streams, cwd, readStdin, adapter) {
2553
2668
  }
2554
2669
  // Minted once, here, and carried into `gateAndWait`: the loop-escalation
2555
2670
  // check below and any registration that follows must name the same task.
2556
- const task = `hook:${input.sessionId}:${input.toolUseId ?? randomBytes(8).toString("hex")}`;
2671
+ const task = adapter.kind === "codex"
2672
+ ? codexBinding(input, cwd).task
2673
+ : `hook:${input.sessionId}:${input.toolUseId ?? randomBytes(8).toString("hex")}`;
2557
2674
  const run = {
2558
2675
  logPath,
2559
2676
  options,
@@ -2564,6 +2681,7 @@ function runHarnessHook(argv, streams, cwd, readStdin, adapter) {
2564
2681
  ttlMs: load.durations.approvalTtlMs,
2565
2682
  harness: adapter.kind,
2566
2683
  originApp: adapter.originApp,
2684
+ ...(codexCommand === undefined ? {} : { codexCommand }),
2567
2685
  eventVersion: input.harnessVersion,
2568
2686
  // Off the policy this function already loaded and validated, so the names
2569
2687
  // printed are the names a channel process would serve (APRV-281). Sorted
@@ -2659,20 +2777,6 @@ function runHarnessHook(argv, streams, cwd, readStdin, adapter) {
2659
2777
  * refinement's own words, and the loop floor's when one applied.
2660
2778
  */
2661
2779
  const note = notes.length === 0 ? "" : ` (${notes.join("; ")})`;
2662
- // APRV-303, and the last thing that can answer without touching the log: an
2663
- // ordinary workspace edit, which the policy allows on its own merits and which
2664
- // is a question only while a floor stands.
2665
- //
2666
- // The order is the whole fix. Until APRV-303 this allow was printed from
2667
- // `describeToolCall`'s own branch, several hundred lines above the floor
2668
- // lookup, so a session whose Bash calls were all being routed to a human went
2669
- // on editing files unrouted and uncounted. Now the same allow is printed, in
2670
- // the same words, from BELOW the floor: the fast path is as fast as it was,
2671
- // and the floored path routes an Edit exactly as it routes an `echo >`,
2672
- // because both are `files.write.workspace` and one predicate decides.
2673
- if (described.passthrough !== undefined && floor === null) {
2674
- return allow(streams, `${described.passthrough}${note}`, adapter.kind);
2675
- }
2676
2780
  /** No class here needs a human, so nothing downstream will ask for one. */
2677
2781
  const unattended = floor === null && autonomies.every((autonomy) => autonomy !== "manual");
2678
2782
  if (unattended) {
@@ -2690,7 +2794,7 @@ function runHarnessHook(argv, streams, cwd, readStdin, adapter) {
2690
2794
  if (charged !== null) {
2691
2795
  return deny(streams, `hook-gate-refused:${charged.code}`, charged.message, adapter.kind);
2692
2796
  }
2693
- return allow(streams, `autonomous: ${classes.join(", ")}${note}`, adapter.kind);
2797
+ return allow(streams, `autonomous: ${classes.join(", ")}${note}`, adapter.kind, codexCommand);
2694
2798
  }
2695
2799
  // Past here the hook appends. It writes to a log that already exists and
2696
2800
  // creates none: a log the hook scaffolded where it happened to be standing
@@ -2734,6 +2838,8 @@ export function commandHook(argv, streams, cwd, readStdin = defaultStdin) {
2734
2838
  return commandHarnessHook(rest, streams, cwd, readStdin, CLAUDE_ADAPTER);
2735
2839
  case "cursor":
2736
2840
  return commandHarnessHook(rest, streams, cwd, readStdin, CURSOR_ADAPTER);
2841
+ case "codex":
2842
+ return commandHarnessHook(rest, streams, cwd, readStdin, CODEX_ADAPTER);
2737
2843
  case "classify":
2738
2844
  return commandClassify(rest, streams, cwd);
2739
2845
  default: