cclaw-cli 0.39.0 → 0.40.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.
- package/README.md +22 -14
- package/dist/cli.js +88 -4
- package/dist/codex-feature-flag.d.ts +58 -0
- package/dist/codex-feature-flag.js +193 -0
- package/dist/constants.d.ts +1 -1
- package/dist/constants.js +7 -4
- package/dist/content/harness-playbooks.js +64 -30
- package/dist/content/harnesses-doc.js +1 -1
- package/dist/content/hook-events.js +11 -5
- package/dist/content/hooks.js +2 -2
- package/dist/content/observe.d.ts +19 -0
- package/dist/content/observe.js +29 -13
- package/dist/doctor.js +86 -21
- package/dist/harness-adapters.js +87 -41
- package/dist/install.js +28 -15
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -127,9 +127,12 @@ Plus harness-specific shims:
|
|
|
127
127
|
- `.claude/commands/cc*.md` + `.claude/hooks/hooks.json`
|
|
128
128
|
- `.cursor/commands/cc*.md` + `.cursor/hooks.json` + `.cursor/rules/cclaw-workflow.mdc`
|
|
129
129
|
- `.opencode/commands/cc*.md` + `.opencode/plugins/cclaw-plugin.mjs`
|
|
130
|
-
- `.agents/skills/
|
|
131
|
-
or description-based auto-matching
|
|
132
|
-
|
|
130
|
+
- `.agents/skills/cc*/SKILL.md` + `.codex/hooks.json` (Codex; skills are
|
|
131
|
+
activated via `/use cc` or description-based auto-matching. Hooks
|
|
132
|
+
require Codex CLI ≥ v0.114 and `[features] codex_hooks = true` in
|
|
133
|
+
`~/.codex/config.toml`; `cclaw init --codex` offers to patch that flag
|
|
134
|
+
for you. `.codex/commands/` and the legacy `.agents/skills/cclaw-cc*/`
|
|
135
|
+
folders are auto-cleaned on sync.)
|
|
133
136
|
- `AGENTS.md` with a managed routing block (includes a Codex-specific note)
|
|
134
137
|
|
|
135
138
|
`.cclaw/config.yaml` holds every tunable key (prompt guard strictness,
|
|
@@ -357,7 +360,7 @@ closes every real gap with a documented fallback — not a silent waiver.
|
|
|
357
360
|
| Claude Code | full (named subagents) | `native` | full | `AskUserQuestion` | [`claude-playbook.md`](./src/content/harness-playbooks.ts) |
|
|
358
361
|
| Cursor | generic Task dispatcher | `generic-dispatch` | full | `AskQuestion` | `cursor-playbook.md` |
|
|
359
362
|
| OpenCode | plugin / in-session | `role-switch` | plugin | plain-text | `opencode-playbook.md` |
|
|
360
|
-
| OpenAI Codex | in-session only | `role-switch` (evidenceRefs required) |
|
|
363
|
+
| OpenAI Codex | in-session only | `role-switch` (evidenceRefs required) | limited (Bash-only `PreToolUse`/`PostToolUse`; requires `codex_hooks` feature flag) | plain-text | `codex-playbook.md` |
|
|
361
364
|
|
|
362
365
|
What the fallbacks mean:
|
|
363
366
|
|
|
@@ -380,16 +383,21 @@ What the fallbacks mean:
|
|
|
380
383
|
harness declares it. Currently unused — v0.33 removed the old
|
|
381
384
|
Codex-only auto-waiver path.
|
|
382
385
|
|
|
383
|
-
> **Codex note (v0.
|
|
384
|
-
>
|
|
385
|
-
>
|
|
386
|
-
>
|
|
387
|
-
> `/use
|
|
388
|
-
>
|
|
389
|
-
>
|
|
390
|
-
>
|
|
391
|
-
>
|
|
392
|
-
>
|
|
386
|
+
> **Codex note (v0.40+).** Codex CLI deprecated custom prompts in v0.89
|
|
387
|
+
> (Jan 2026), but Codex ≥ v0.114 (Mar 2026) grew an experimental
|
|
388
|
+
> lifecycle hooks API. cclaw installs Codex entry points as native
|
|
389
|
+
> **skills** under `.agents/skills/cc*/SKILL.md` (invoke with `/use cc`,
|
|
390
|
+
> `/use cc-next`, `/use cc-view`, `/use cc-ops`, `/use cc-ideate`, or
|
|
391
|
+
> by typing `/cc …` in plain text — Codex auto-matches from the skill
|
|
392
|
+
> description) **and** writes `.codex/hooks.json` so session-start
|
|
393
|
+
> rehydration, stop-checkpoint, prompt-guard, workflow-guard, and
|
|
394
|
+
> context-monitor fire automatically — as long as you enable the
|
|
395
|
+
> `codex_hooks` feature flag in `~/.codex/config.toml`. `cclaw init
|
|
396
|
+
> --codex` asks for consent before patching that file. Codex's
|
|
397
|
+
> `PreToolUse`/`PostToolUse` are Bash-only; the stage skills compensate
|
|
398
|
+
> for `Write`/`Edit`/`MCP` tool calls with explicit in-turn checks. Run
|
|
399
|
+
> `cclaw doctor` to see the current state of hooks, the feature flag,
|
|
400
|
+
> and any legacy layout to clean up.
|
|
393
401
|
|
|
394
402
|
The full capability matrix lives in
|
|
395
403
|
[`docs/harnesses.md`](./docs/harnesses.md). Per-harness playbooks are
|
package/dist/cli.js
CHANGED
|
@@ -15,6 +15,7 @@ import { CCLAW_VERSION, RUNTIME_ROOT } from "./constants.js";
|
|
|
15
15
|
import { createDefaultConfig } from "./config.js";
|
|
16
16
|
import { detectHarnesses } from "./init-detect.js";
|
|
17
17
|
import { HARNESS_ADAPTERS } from "./harness-adapters.js";
|
|
18
|
+
import { classifyCodexHooksFlag, codexConfigPath, patchCodexHooksFlag, readCodexConfig, writeCodexConfig } from "./codex-feature-flag.js";
|
|
18
19
|
import { runEval } from "./eval/runner.js";
|
|
19
20
|
import { createStderrProgressLogger } from "./eval/progress.js";
|
|
20
21
|
import { writeBaselinesFromReport } from "./eval/baseline.js";
|
|
@@ -152,7 +153,7 @@ function buildInitSurfacePreview(harnesses) {
|
|
|
152
153
|
for (const harness of harnesses) {
|
|
153
154
|
const adapter = HARNESS_ADAPTERS[harness];
|
|
154
155
|
if (adapter.shimKind === "skill") {
|
|
155
|
-
lines.push(`${adapter.commandDir}/
|
|
156
|
+
lines.push(`${adapter.commandDir}/cc*/SKILL.md`);
|
|
156
157
|
}
|
|
157
158
|
else {
|
|
158
159
|
lines.push(`${adapter.commandDir}/cc*.md`);
|
|
@@ -164,9 +165,12 @@ function buildInitSurfacePreview(harnesses) {
|
|
|
164
165
|
lines.push(".cursor/hooks.json");
|
|
165
166
|
lines.push(".cursor/rules/cclaw-workflow.mdc");
|
|
166
167
|
}
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
168
|
+
if (harness === "codex") {
|
|
169
|
+
// v0.40.0: .codex/hooks.json is managed again now that Codex CLI
|
|
170
|
+
// grew a real hooks API (v0.114+, behind the `codex_hooks`
|
|
171
|
+
// feature flag). Legacy `.codex/commands/*` is still auto-cleaned.
|
|
172
|
+
lines.push(".codex/hooks.json (requires `codex_hooks = true` in ~/.codex/config.toml)");
|
|
173
|
+
}
|
|
170
174
|
if (harness === "opencode") {
|
|
171
175
|
lines.push(".opencode/plugins/cclaw-plugin.mjs");
|
|
172
176
|
lines.push("opencode.json(.c) plugin registration");
|
|
@@ -207,6 +211,85 @@ async function promptInitConfig(defaults, ctx) {
|
|
|
207
211
|
rl.close();
|
|
208
212
|
}
|
|
209
213
|
}
|
|
214
|
+
/**
|
|
215
|
+
* When Codex is one of the installed harnesses, check the Codex CLI
|
|
216
|
+
* config file for the `codex_hooks` feature flag. If it is missing or
|
|
217
|
+
* disabled, offer to patch it in with the user's explicit consent.
|
|
218
|
+
*
|
|
219
|
+
* The function is deliberately advisory: it never fails init — the worst
|
|
220
|
+
* case is that Codex runs without the hooks engine, which is exactly
|
|
221
|
+
* how v0.39.x already shipped. We always print a resolution hint so
|
|
222
|
+
* the user knows what to do next regardless of which branch was taken.
|
|
223
|
+
*/
|
|
224
|
+
async function maybeEnableCodexHooksFlag(harnesses, parsed, ctx) {
|
|
225
|
+
if (!harnesses || !harnesses.includes("codex"))
|
|
226
|
+
return;
|
|
227
|
+
const configPath = codexConfigPath();
|
|
228
|
+
let existing;
|
|
229
|
+
try {
|
|
230
|
+
existing = await readCodexConfig(configPath);
|
|
231
|
+
}
|
|
232
|
+
catch (err) {
|
|
233
|
+
ctx.stdout.write(`note: Could not read ${configPath} to check the codex_hooks flag: ` +
|
|
234
|
+
`${err instanceof Error ? err.message : String(err)}\n`);
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
const state = classifyCodexHooksFlag(existing);
|
|
238
|
+
if (state === "enabled") {
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
const humanState = state === "missing-file"
|
|
242
|
+
? "Codex config file does not exist yet"
|
|
243
|
+
: state === "missing-section"
|
|
244
|
+
? "no [features] section"
|
|
245
|
+
: state === "missing-key"
|
|
246
|
+
? "no codex_hooks key"
|
|
247
|
+
: "codex_hooks is not enabled";
|
|
248
|
+
const instructions = `To enable Codex hooks manually later, ensure ${configPath} contains:\n` +
|
|
249
|
+
` [features]\n codex_hooks = true\n`;
|
|
250
|
+
if (parsed.interactive === false) {
|
|
251
|
+
ctx.stdout.write(`note: codex_hooks feature flag is not enabled (${humanState}).\n` +
|
|
252
|
+
` cclaw wrote .codex/hooks.json, but Codex will ignore it until you enable the flag.\n` +
|
|
253
|
+
` ${instructions}`);
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
if (!isInitPromptAllowed(ctx)) {
|
|
257
|
+
ctx.stdout.write(`note: codex_hooks feature flag is not enabled (${humanState}).\n` +
|
|
258
|
+
` cclaw wrote .codex/hooks.json, but Codex will ignore it until you enable the flag.\n` +
|
|
259
|
+
` ${instructions}`);
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
const rl = createInterface({
|
|
263
|
+
input: process.stdin,
|
|
264
|
+
output: ctx.stdout
|
|
265
|
+
});
|
|
266
|
+
try {
|
|
267
|
+
const answer = (await rl.question(`\nCodex CLI hooks are off (${humanState}).\n` +
|
|
268
|
+
`Enable [features] codex_hooks = true in ${configPath} now? [y/N]: `)).trim().toLowerCase();
|
|
269
|
+
const yes = answer === "y" || answer === "yes";
|
|
270
|
+
if (!yes) {
|
|
271
|
+
ctx.stdout.write(`Leaving ${configPath} untouched. ${instructions}`);
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
const { updated, changed } = patchCodexHooksFlag(existing);
|
|
275
|
+
if (!changed) {
|
|
276
|
+
ctx.stdout.write(`codex_hooks is already enabled — no changes written.\n`);
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
try {
|
|
280
|
+
await writeCodexConfig(configPath, updated);
|
|
281
|
+
ctx.stdout.write(`Enabled [features] codex_hooks = true in ${configPath}.\n`);
|
|
282
|
+
}
|
|
283
|
+
catch (err) {
|
|
284
|
+
ctx.stdout.write(`Could not write ${configPath}: ` +
|
|
285
|
+
`${err instanceof Error ? err.message : String(err)}\n` +
|
|
286
|
+
`${instructions}`);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
finally {
|
|
290
|
+
rl.close();
|
|
291
|
+
}
|
|
292
|
+
}
|
|
210
293
|
async function resolveInitInputs(parsed, ctx) {
|
|
211
294
|
const detectedHarnesses = parsed.harnesses ? [] : await detectHarnesses(ctx.cwd);
|
|
212
295
|
const autoHarnesses = parsed.harnesses
|
|
@@ -744,6 +827,7 @@ async function runCommand(parsed, ctx) {
|
|
|
744
827
|
}
|
|
745
828
|
const trackNote = effectiveTrack ? ` (track=${effectiveTrack})` : "";
|
|
746
829
|
info(ctx, `Initialized .cclaw runtime and generated harness shims${trackNote}`);
|
|
830
|
+
await maybeEnableCodexHooksFlag(effectiveHarnesses, parsed, ctx);
|
|
747
831
|
return 0;
|
|
748
832
|
}
|
|
749
833
|
if (command === "sync") {
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Manage the `codex_hooks` feature flag in `~/.codex/config.toml`.
|
|
3
|
+
*
|
|
4
|
+
* Codex CLI ≥ v0.114 (Mar 2026) exposes lifecycle hooks via
|
|
5
|
+
* `.codex/hooks.json`, but the hooks engine is inert unless the user has
|
|
6
|
+
* opted into it with:
|
|
7
|
+
*
|
|
8
|
+
* ```toml
|
|
9
|
+
* [features]
|
|
10
|
+
* codex_hooks = true
|
|
11
|
+
* ```
|
|
12
|
+
*
|
|
13
|
+
* in `$CODEX_HOME/config.toml` (default: `~/.codex/config.toml`).
|
|
14
|
+
* cclaw's `init --codex` prompts the user to flip this flag for them;
|
|
15
|
+
* this module owns the detection / mutation code so the prompt logic in
|
|
16
|
+
* `cli.ts` stays small and testable.
|
|
17
|
+
*
|
|
18
|
+
* The TOML mutations here are intentionally surgical — we never reparse
|
|
19
|
+
* or rewrite the whole document. A deliberately narrow regex based
|
|
20
|
+
* approach lets the function stay dependency-free and preserves the
|
|
21
|
+
* user's comments, whitespace, and custom key ordering.
|
|
22
|
+
*/
|
|
23
|
+
/**
|
|
24
|
+
* Absolute path of the Codex config file. Respects `$CODEX_HOME` when
|
|
25
|
+
* present (the only override Codex CLI documents); falls back to
|
|
26
|
+
* `~/.codex/config.toml` otherwise.
|
|
27
|
+
*/
|
|
28
|
+
export declare function codexConfigPath(env?: NodeJS.ProcessEnv): string;
|
|
29
|
+
export type CodexHooksFlagState = "enabled" | "disabled" | "missing-key" | "missing-section" | "missing-file";
|
|
30
|
+
/**
|
|
31
|
+
* Inspect a TOML document and decide which of the five canonical states
|
|
32
|
+
* it represents. Comments and blank lines are ignored. Only the first
|
|
33
|
+
* `[features]` section is considered — duplicates are technically invalid
|
|
34
|
+
* TOML and Codex rejects them, so cclaw does not try to be clever there.
|
|
35
|
+
*/
|
|
36
|
+
export declare function classifyCodexHooksFlag(toml: string | null): CodexHooksFlagState;
|
|
37
|
+
/**
|
|
38
|
+
* Return a TOML document with `[features] codex_hooks = true` set.
|
|
39
|
+
* Preserves all other content verbatim:
|
|
40
|
+
* - If the document lacks a `[features]` section, we append one at the
|
|
41
|
+
* end of the file (separated by a blank line).
|
|
42
|
+
* - If `[features]` exists without `codex_hooks`, we insert the key
|
|
43
|
+
* immediately after the header.
|
|
44
|
+
* - If `codex_hooks` exists with any non-`true` value, we rewrite
|
|
45
|
+
* just that line.
|
|
46
|
+
* - If the flag is already `true`, the input is returned unchanged.
|
|
47
|
+
*/
|
|
48
|
+
export declare function patchCodexHooksFlag(toml: string | null): {
|
|
49
|
+
updated: string;
|
|
50
|
+
changed: boolean;
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* Read the Codex config, return `null` when the file does not exist.
|
|
54
|
+
* All other read errors propagate so callers can surface a useful
|
|
55
|
+
* message instead of silently degrading.
|
|
56
|
+
*/
|
|
57
|
+
export declare function readCodexConfig(configPath: string): Promise<string | null>;
|
|
58
|
+
export declare function writeCodexConfig(configPath: string, content: string): Promise<void>;
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Manage the `codex_hooks` feature flag in `~/.codex/config.toml`.
|
|
3
|
+
*
|
|
4
|
+
* Codex CLI ≥ v0.114 (Mar 2026) exposes lifecycle hooks via
|
|
5
|
+
* `.codex/hooks.json`, but the hooks engine is inert unless the user has
|
|
6
|
+
* opted into it with:
|
|
7
|
+
*
|
|
8
|
+
* ```toml
|
|
9
|
+
* [features]
|
|
10
|
+
* codex_hooks = true
|
|
11
|
+
* ```
|
|
12
|
+
*
|
|
13
|
+
* in `$CODEX_HOME/config.toml` (default: `~/.codex/config.toml`).
|
|
14
|
+
* cclaw's `init --codex` prompts the user to flip this flag for them;
|
|
15
|
+
* this module owns the detection / mutation code so the prompt logic in
|
|
16
|
+
* `cli.ts` stays small and testable.
|
|
17
|
+
*
|
|
18
|
+
* The TOML mutations here are intentionally surgical — we never reparse
|
|
19
|
+
* or rewrite the whole document. A deliberately narrow regex based
|
|
20
|
+
* approach lets the function stay dependency-free and preserves the
|
|
21
|
+
* user's comments, whitespace, and custom key ordering.
|
|
22
|
+
*/
|
|
23
|
+
import fs from "node:fs/promises";
|
|
24
|
+
import os from "node:os";
|
|
25
|
+
import path from "node:path";
|
|
26
|
+
/**
|
|
27
|
+
* Absolute path of the Codex config file. Respects `$CODEX_HOME` when
|
|
28
|
+
* present (the only override Codex CLI documents); falls back to
|
|
29
|
+
* `~/.codex/config.toml` otherwise.
|
|
30
|
+
*/
|
|
31
|
+
export function codexConfigPath(env = process.env) {
|
|
32
|
+
const codexHome = env.CODEX_HOME && env.CODEX_HOME.trim().length > 0
|
|
33
|
+
? env.CODEX_HOME
|
|
34
|
+
: path.join(os.homedir(), ".codex");
|
|
35
|
+
return path.join(codexHome, "config.toml");
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Inspect a TOML document and decide which of the five canonical states
|
|
39
|
+
* it represents. Comments and blank lines are ignored. Only the first
|
|
40
|
+
* `[features]` section is considered — duplicates are technically invalid
|
|
41
|
+
* TOML and Codex rejects them, so cclaw does not try to be clever there.
|
|
42
|
+
*/
|
|
43
|
+
export function classifyCodexHooksFlag(toml) {
|
|
44
|
+
if (toml === null) {
|
|
45
|
+
return "missing-file";
|
|
46
|
+
}
|
|
47
|
+
const lines = toml.split(/\r?\n/);
|
|
48
|
+
let inFeaturesSection = false;
|
|
49
|
+
let sawFeaturesHeader = false;
|
|
50
|
+
for (const rawLine of lines) {
|
|
51
|
+
const stripped = stripTomlComment(rawLine).trim();
|
|
52
|
+
if (stripped.length === 0)
|
|
53
|
+
continue;
|
|
54
|
+
const headerMatch = /^\[\s*([A-Za-z0-9_.-]+)\s*\]$/u.exec(stripped);
|
|
55
|
+
if (headerMatch) {
|
|
56
|
+
const section = headerMatch[1];
|
|
57
|
+
if (section === "features") {
|
|
58
|
+
inFeaturesSection = true;
|
|
59
|
+
sawFeaturesHeader = true;
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
inFeaturesSection = false;
|
|
63
|
+
}
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (inFeaturesSection) {
|
|
67
|
+
const keyMatch = /^codex_hooks\s*=\s*(.*)$/u.exec(stripped);
|
|
68
|
+
if (keyMatch) {
|
|
69
|
+
const value = keyMatch[1].trim().toLowerCase();
|
|
70
|
+
return value === "true" ? "enabled" : "disabled";
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (sawFeaturesHeader)
|
|
75
|
+
return "missing-key";
|
|
76
|
+
return "missing-section";
|
|
77
|
+
}
|
|
78
|
+
function stripTomlComment(line) {
|
|
79
|
+
// Naive but sufficient for our narrow use case: we only read cclaw's
|
|
80
|
+
// own writes back, and cclaw never emits `=` after a `#` inside a
|
|
81
|
+
// string literal in config.toml. If a user has complex string values
|
|
82
|
+
// with `#` inside them, worst case we trip `classifyCodexHooksFlag`
|
|
83
|
+
// and prompt them again — non-destructive.
|
|
84
|
+
const hashIndex = line.indexOf("#");
|
|
85
|
+
return hashIndex === -1 ? line : line.slice(0, hashIndex);
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Return a TOML document with `[features] codex_hooks = true` set.
|
|
89
|
+
* Preserves all other content verbatim:
|
|
90
|
+
* - If the document lacks a `[features]` section, we append one at the
|
|
91
|
+
* end of the file (separated by a blank line).
|
|
92
|
+
* - If `[features]` exists without `codex_hooks`, we insert the key
|
|
93
|
+
* immediately after the header.
|
|
94
|
+
* - If `codex_hooks` exists with any non-`true` value, we rewrite
|
|
95
|
+
* just that line.
|
|
96
|
+
* - If the flag is already `true`, the input is returned unchanged.
|
|
97
|
+
*/
|
|
98
|
+
export function patchCodexHooksFlag(toml) {
|
|
99
|
+
const initial = toml ?? "";
|
|
100
|
+
const state = classifyCodexHooksFlag(toml);
|
|
101
|
+
if (state === "enabled") {
|
|
102
|
+
return { updated: initial, changed: false };
|
|
103
|
+
}
|
|
104
|
+
if (state === "missing-file" || state === "missing-section") {
|
|
105
|
+
const prefix = initial.length === 0
|
|
106
|
+
? ""
|
|
107
|
+
: initial.endsWith("\n") ? initial : `${initial}\n`;
|
|
108
|
+
const separator = initial.trim().length === 0 ? "" : "\n";
|
|
109
|
+
const block = `${separator}[features]\ncodex_hooks = true\n`;
|
|
110
|
+
return { updated: `${prefix}${block}`, changed: true };
|
|
111
|
+
}
|
|
112
|
+
if (state === "missing-key") {
|
|
113
|
+
const updated = insertKeyInFeaturesSection(initial);
|
|
114
|
+
return { updated, changed: true };
|
|
115
|
+
}
|
|
116
|
+
const updated = replaceCodexHooksLineInFeaturesSection(initial);
|
|
117
|
+
return { updated, changed: true };
|
|
118
|
+
}
|
|
119
|
+
function insertKeyInFeaturesSection(toml) {
|
|
120
|
+
// Walk into `[features]`, remember the index of the last key / non-blank
|
|
121
|
+
// line inside that section, and splice `codex_hooks = true` immediately
|
|
122
|
+
// after it. This keeps the inserted key adjacent to existing features,
|
|
123
|
+
// never stranded after a blank line or pushed down past a later section
|
|
124
|
+
// header. If `[features]` is empty, we insert right after its header.
|
|
125
|
+
const lines = toml.split(/\r?\n/);
|
|
126
|
+
let inFeaturesSection = false;
|
|
127
|
+
let featuresHeaderIndex = -1;
|
|
128
|
+
let lastFeatureKeyIndex = -1;
|
|
129
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
130
|
+
const rawLine = lines[index];
|
|
131
|
+
const stripped = stripTomlComment(rawLine).trim();
|
|
132
|
+
const headerMatch = /^\[\s*([A-Za-z0-9_.-]+)\s*\]$/u.exec(stripped);
|
|
133
|
+
if (headerMatch) {
|
|
134
|
+
if (inFeaturesSection)
|
|
135
|
+
break;
|
|
136
|
+
if (headerMatch[1] === "features") {
|
|
137
|
+
inFeaturesSection = true;
|
|
138
|
+
featuresHeaderIndex = index;
|
|
139
|
+
lastFeatureKeyIndex = index;
|
|
140
|
+
}
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
if (inFeaturesSection && stripped.length > 0) {
|
|
144
|
+
lastFeatureKeyIndex = index;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
if (featuresHeaderIndex === -1) {
|
|
148
|
+
// caller should have short-circuited before getting here; defensive
|
|
149
|
+
return toml;
|
|
150
|
+
}
|
|
151
|
+
lines.splice(lastFeatureKeyIndex + 1, 0, "codex_hooks = true");
|
|
152
|
+
const joined = lines.join("\n");
|
|
153
|
+
return joined.endsWith("\n") ? joined : `${joined}\n`;
|
|
154
|
+
}
|
|
155
|
+
function replaceCodexHooksLineInFeaturesSection(toml) {
|
|
156
|
+
const lines = toml.split(/\r?\n/);
|
|
157
|
+
let inFeaturesSection = false;
|
|
158
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
159
|
+
const rawLine = lines[index];
|
|
160
|
+
const stripped = stripTomlComment(rawLine).trim();
|
|
161
|
+
const headerMatch = /^\[\s*([A-Za-z0-9_.-]+)\s*\]$/u.exec(stripped);
|
|
162
|
+
if (headerMatch) {
|
|
163
|
+
inFeaturesSection = headerMatch[1] === "features";
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
if (inFeaturesSection && /^codex_hooks\s*=/u.test(stripped)) {
|
|
167
|
+
const indent = /^\s*/u.exec(rawLine)?.[0] ?? "";
|
|
168
|
+
lines[index] = `${indent}codex_hooks = true`;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
const joined = lines.join("\n");
|
|
172
|
+
return joined.endsWith("\n") ? joined : `${joined}\n`;
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Read the Codex config, return `null` when the file does not exist.
|
|
176
|
+
* All other read errors propagate so callers can surface a useful
|
|
177
|
+
* message instead of silently degrading.
|
|
178
|
+
*/
|
|
179
|
+
export async function readCodexConfig(configPath) {
|
|
180
|
+
try {
|
|
181
|
+
return await fs.readFile(configPath, "utf8");
|
|
182
|
+
}
|
|
183
|
+
catch (err) {
|
|
184
|
+
if (err.code === "ENOENT") {
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
throw err;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
export async function writeCodexConfig(configPath, content) {
|
|
191
|
+
await fs.mkdir(path.dirname(configPath), { recursive: true });
|
|
192
|
+
await fs.writeFile(configPath, content, "utf8");
|
|
193
|
+
}
|
package/dist/constants.d.ts
CHANGED
|
@@ -14,7 +14,7 @@ export declare const EVALS_ROOT = ".cclaw/evals";
|
|
|
14
14
|
export declare const EVALS_CONFIG_PATH = ".cclaw/evals/config.yaml";
|
|
15
15
|
export declare const EVALS_DIRS: readonly [".cclaw/evals", ".cclaw/evals/corpus", ".cclaw/evals/rubrics", ".cclaw/evals/baselines", ".cclaw/evals/reports"];
|
|
16
16
|
export declare const REQUIRED_DIRS: readonly [".cclaw", ".cclaw/commands", ".cclaw/skills", ".cclaw/contexts", ".cclaw/templates", ".cclaw/artifacts", ".cclaw/worktrees", ".cclaw/state", ".cclaw/runs", ".cclaw/rules", ".cclaw/adapters", ".cclaw/agents", ".cclaw/hooks", ".cclaw/custom-skills", ".cclaw/evals", ".cclaw/evals/corpus", ".cclaw/evals/rubrics", ".cclaw/evals/baselines", ".cclaw/evals/reports"];
|
|
17
|
-
export declare const REQUIRED_GITIGNORE_PATTERNS: readonly ["# cclaw generated artifacts", ".cclaw/", "# cclaw evals: user-owned, track in git", "!.cclaw/evals/", "!.cclaw/evals/config.yaml", "!.cclaw/evals/corpus/", "!.cclaw/evals/corpus/**", "!.cclaw/evals/rubrics/", "!.cclaw/evals/rubrics/**", "!.cclaw/evals/baselines/", "!.cclaw/evals/baselines/**", ".claude/commands/cc-*.md", ".claude/commands/cc.md", ".cursor/commands/cc-*.md", ".cursor/commands/cc.md", ".opencode/commands/cc-*.md", ".opencode/commands/cc.md", ".agents/skills/
|
|
17
|
+
export declare const REQUIRED_GITIGNORE_PATTERNS: readonly ["# cclaw generated artifacts", ".cclaw/", "# cclaw evals: user-owned, track in git", "!.cclaw/evals/", "!.cclaw/evals/config.yaml", "!.cclaw/evals/corpus/", "!.cclaw/evals/corpus/**", "!.cclaw/evals/rubrics/", "!.cclaw/evals/rubrics/**", "!.cclaw/evals/baselines/", "!.cclaw/evals/baselines/**", ".claude/commands/cc-*.md", ".claude/commands/cc.md", ".cursor/commands/cc-*.md", ".cursor/commands/cc.md", ".opencode/commands/cc-*.md", ".opencode/commands/cc.md", ".agents/skills/cc/SKILL.md", ".agents/skills/cc-*/SKILL.md", ".claude/hooks/hooks.json", ".cursor/hooks.json", ".codex/hooks.json", ".opencode/plugins/cclaw-plugin.mjs", ".cursor/rules/cclaw-workflow.mdc"];
|
|
18
18
|
export declare const COMMAND_FILE_ORDER: FlowStage[];
|
|
19
19
|
export declare const UTILITY_COMMANDS: readonly ["learn", "next", "ideate", "view", "status", "tree", "diff", "ops", "feature", "tdd-log", "retro", "compound", "archive", "rewind"];
|
|
20
20
|
export declare const SUBAGENT_SKILL_FOLDERS: readonly ["subagent-dev", "parallel-dispatch"];
|
package/dist/constants.js
CHANGED
|
@@ -91,12 +91,15 @@ export const REQUIRED_GITIGNORE_PATTERNS = [
|
|
|
91
91
|
".cursor/commands/cc.md",
|
|
92
92
|
".opencode/commands/cc-*.md",
|
|
93
93
|
".opencode/commands/cc.md",
|
|
94
|
-
// Codex uses skill-kind shims under `.agents/skills/
|
|
95
|
-
// v0.
|
|
96
|
-
|
|
97
|
-
|
|
94
|
+
// Codex uses skill-kind shims under `.agents/skills/cc*/` since
|
|
95
|
+
// v0.40.0 (renamed from the `cclaw-cc*` layout in v0.39.0/v0.39.1).
|
|
96
|
+
// `cclaw sync` and `cclaw uninstall` both auto-remove the legacy
|
|
97
|
+
// `cclaw-cc*` directories.
|
|
98
|
+
".agents/skills/cc/SKILL.md",
|
|
99
|
+
".agents/skills/cc-*/SKILL.md",
|
|
98
100
|
".claude/hooks/hooks.json",
|
|
99
101
|
".cursor/hooks.json",
|
|
102
|
+
".codex/hooks.json",
|
|
100
103
|
".opencode/plugins/cclaw-plugin.mjs",
|
|
101
104
|
".cursor/rules/cclaw-workflow.mdc"
|
|
102
105
|
];
|
|
@@ -192,28 +192,38 @@ has either a \`completed\` row with evidenceRefs (role-switch) or a
|
|
|
192
192
|
const CODEX_PLAYBOOK = `---
|
|
193
193
|
harness: codex
|
|
194
194
|
fallback: role-switch
|
|
195
|
-
description: "OpenAI Codex
|
|
195
|
+
description: "OpenAI Codex exposes lifecycle hooks (v0.114+, gated by the codex_hooks feature flag) but no subagent dispatch and no custom slash commands. cclaw ships entry points as skills under .agents/skills/cc*/ and wires .codex/hooks.json; mandatory delegations fall back to role-switch with evidenceRefs."
|
|
196
196
|
---
|
|
197
197
|
|
|
198
198
|
# OpenAI Codex — Parity Playbook
|
|
199
199
|
|
|
200
|
-
Codex CLI
|
|
201
|
-
API**. cclaw v0.39.0 acknowledged this and rewired the codex harness:
|
|
200
|
+
Codex CLI has a different shape from Claude/Cursor:
|
|
202
201
|
|
|
203
202
|
- **Entry points are skills.** \`/cc\`, \`/cc-next\`, \`/cc-ideate\`,
|
|
204
203
|
\`/cc-view\`, \`/cc-ops\` are generated as skills at
|
|
205
|
-
\`.agents/skills/
|
|
206
|
-
activate via Codex's native
|
|
207
|
-
automatically when the user's prompt
|
|
208
|
-
\`/cc\`-style tokens (skill descriptions include
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
204
|
+
\`.agents/skills/cc/SKILL.md\` (and \`cc-next/\`, \`cc-view/\`,
|
|
205
|
+
\`cc-ideate/\`, \`cc-ops/\`). They activate via Codex's native
|
|
206
|
+
\`/use <skillName>\` command or automatically when the user's prompt
|
|
207
|
+
mentions any of the \`/cc\`-style tokens (skill descriptions include
|
|
208
|
+
them verbatim). Codex CLI removed custom prompts in v0.89 (Jan 2026);
|
|
209
|
+
there is no way to register a true custom slash command.
|
|
210
|
+
- **Lifecycle hooks.** Codex CLI ≥ v0.114 (Mar 2026) exposes lifecycle
|
|
211
|
+
hooks at \`.codex/hooks.json\`, gated behind the experimental
|
|
212
|
+
\`[features] codex_hooks = true\` flag in \`~/.codex/config.toml\`.
|
|
213
|
+
cclaw writes \`.codex/hooks.json\` on sync; if the flag is off, the
|
|
214
|
+
file is simply inert and \`cclaw doctor\` emits a warning. \`cclaw init\`
|
|
215
|
+
offers to patch the flag with explicit user consent.
|
|
216
|
+
- **Tool interception is Bash-only.** Codex's \`PreToolUse\` and
|
|
217
|
+
\`PostToolUse\` events only fire for the \`Bash\` tool. \`Write\`,
|
|
218
|
+
\`Edit\`, \`WebSearch\`, and MCP tool calls are **not** gated by hooks.
|
|
219
|
+
cclaw partially compensates by also wiring \`UserPromptSubmit\` to
|
|
220
|
+
\`prompt-guard.sh\` so the stage routing check fires before the turn
|
|
221
|
+
executes, but workflow-guard (TDD red-first, artifact presence) only
|
|
222
|
+
fires on Bash turns. See the hook coverage matrix below.
|
|
223
|
+
- **Legacy paths.** \`.codex/commands/*\` was never consumed by Codex and
|
|
224
|
+
is removed on every \`cclaw sync\`. The v0.39.x \`.agents/skills/cclaw-cc*/\`
|
|
225
|
+
layout is replaced by \`.agents/skills/cc*/\` and the old folders are
|
|
226
|
+
auto-removed on sync. Do not restore either by hand.
|
|
217
227
|
|
|
218
228
|
## Fallback: role-switch
|
|
219
229
|
|
|
@@ -243,33 +253,57 @@ disabled in v0.33 and remains off.
|
|
|
243
253
|
|
|
244
254
|
## Invocation cheatsheet
|
|
245
255
|
|
|
246
|
-
- \`/use
|
|
247
|
-
- \`/use
|
|
248
|
-
- \`/use
|
|
256
|
+
- \`/use cc\` — open the \`/cc\` skill and pick a track.
|
|
257
|
+
- \`/use cc-next\` — advance the flow one stage.
|
|
258
|
+
- \`/use cc-ops\` — compound / archive / rewind.
|
|
249
259
|
- Typing \`/cc …\` or \`/cc-next …\` in plain text also works: Codex
|
|
250
260
|
matches the skill descriptions (which spell out these tokens) and
|
|
251
261
|
auto-loads the right skill body.
|
|
252
262
|
- Use Codex's built-in \`/skill\` UI to enable or disable
|
|
253
263
|
cclaw skills per session.
|
|
254
264
|
|
|
255
|
-
##
|
|
265
|
+
## Feature flag — how to enable hooks
|
|
256
266
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
267
|
+
Codex CLI ignores \`.codex/hooks.json\` unless \`codex_hooks = true\`
|
|
268
|
+
appears under \`[features]\` in \`~/.codex/config.toml\`:
|
|
269
|
+
|
|
270
|
+
\`\`\`toml
|
|
271
|
+
[features]
|
|
272
|
+
codex_hooks = true
|
|
273
|
+
\`\`\`
|
|
274
|
+
|
|
275
|
+
\`cclaw init --codex\` prompts to write this automatically (one-line
|
|
276
|
+
diff, preserving the rest of \`config.toml\` untouched). Decline the
|
|
277
|
+
prompt to leave the file alone; the skill-level \`/use cc\` entry points
|
|
278
|
+
continue to work regardless.
|
|
279
|
+
|
|
280
|
+
## Hook coverage matrix
|
|
281
|
+
|
|
282
|
+
| Hook intent | Codex mapping | Coverage |
|
|
283
|
+
|-------------|---------------|----------|
|
|
284
|
+
| SessionStart rehydration | \`SessionStart\` matcher \`startup|resume\` → \`session-start.sh\` | Full. |
|
|
285
|
+
| PreToolUse prompt-guard | \`PreToolUse\` matcher \`Bash\` + \`UserPromptSubmit\` → \`prompt-guard.sh\` | Bash tool calls are gated inline; \`UserPromptSubmit\` catches prompts before any tool fires, so non-Bash writes (\`Write\`/\`Edit\`) are still prompt-guarded at the turn boundary. |
|
|
286
|
+
| PreToolUse workflow-guard | \`PreToolUse\` matcher \`Bash\` → \`workflow-guard.sh\` | Bash-only. For \`Write\`/\`Edit\` calls the agent performs the TDD-order / artifact check in-turn (see the stage skill). |
|
|
287
|
+
| PostToolUse context-monitor | \`PostToolUse\` matcher \`Bash\` → \`context-monitor.sh\` | Bash-only. Other tool calls get context-monitored at end-of-turn via \`.cclaw/references/protocols/ethos.md\`. |
|
|
288
|
+
| Stop checkpoint | \`Stop\` → \`stop-checkpoint.sh\` | Full. |
|
|
289
|
+
| PreCompact digest | Not supported — Codex has no \`PreCompact\` event. | Covered by \`/cc-ops retro\` and the user running \`/cc-view status\` before Codex's \`/compact\` command. |
|
|
265
290
|
|
|
266
291
|
## Verification
|
|
267
292
|
|
|
268
293
|
\`cclaw doctor\` on a codex-enabled install checks:
|
|
269
294
|
|
|
270
|
-
- \`shim:codex:
|
|
271
|
-
|
|
272
|
-
-
|
|
295
|
+
- \`shim:codex:cc:present\` and \`frontmatter\` (plus the four utility
|
|
296
|
+
skills \`cc-next\`, \`cc-view\`, \`cc-ops\`, \`cc-ideate\`).
|
|
297
|
+
- \`hook:schema:codex\` validates \`.codex/hooks.json\` shape.
|
|
298
|
+
- \`hook:wiring:codex\` verifies the generated hooks reference every
|
|
299
|
+
runtime script cclaw needs (session-start, prompt-guard, workflow-guard,
|
|
300
|
+
context-monitor, stop-checkpoint).
|
|
301
|
+
- \`warning:codex:feature_flag\` is emitted as a warning (not an error)
|
|
302
|
+
when \`~/.codex/config.toml\` is missing the \`codex_hooks\` feature
|
|
303
|
+
flag — hooks silently do nothing in that state.
|
|
304
|
+
- \`warning:codex:legacy_commands_dir\` and
|
|
305
|
+
\`warning:codex:legacy_cclaw_cc_skills\` catch leftovers from older
|
|
306
|
+
cclaw versions.
|
|
273
307
|
- Every mandatory agent for the active stage has a \`completed\` row
|
|
274
308
|
with \`fulfillmentMode: "role-switch"\` and at least one \`evidenceRef\`.
|
|
275
309
|
`;
|
|
@@ -112,7 +112,7 @@ Harness-specific additions:
|
|
|
112
112
|
- \`claude\`: \`.claude/commands/cc*.md\`, \`.claude/hooks/hooks.json\`
|
|
113
113
|
- \`cursor\`: \`.cursor/commands/cc*.md\`, \`.cursor/hooks.json\`, \`.cursor/rules/cclaw-workflow.mdc\`
|
|
114
114
|
- \`opencode\`: \`.opencode/commands/cc*.md\`, \`.opencode/plugins/cclaw-plugin.mjs\`, opencode plugin registration
|
|
115
|
-
- \`codex\`: \`.agents/skills/
|
|
115
|
+
- \`codex\`: \`.agents/skills/cc/SKILL.md\`, \`.agents/skills/cc-next/SKILL.md\`, \`.agents/skills/cc-ideate/SKILL.md\`, \`.agents/skills/cc-view/SKILL.md\`, \`.agents/skills/cc-ops/SKILL.md\`, \`.codex/hooks.json\` (Codex CLI reads \`.agents/skills/\` for custom skills and consumes \`.codex/hooks.json\` on v0.114+ when \`[features] codex_hooks = true\` is set in \`~/.codex/config.toml\`. \`.codex/commands/\` and the legacy \`.agents/skills/cclaw-cc*/\` layout from v0.39.x are auto-cleaned on sync.)
|
|
116
116
|
|
|
117
117
|
## Runtime observability
|
|
118
118
|
|
|
@@ -32,10 +32,16 @@ export const HOOK_EVENTS_BY_HARNESS = {
|
|
|
32
32
|
precompact_digest: "plugin session.cleared/session.resumed hooks"
|
|
33
33
|
},
|
|
34
34
|
codex: {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
35
|
+
// Codex CLI v0.114+ exposes lifecycle hooks via `.codex/hooks.json`,
|
|
36
|
+
// gated by `[features] codex_hooks = true` in `~/.codex/config.toml`.
|
|
37
|
+
// SessionStart, Stop, and UserPromptSubmit fire for every turn;
|
|
38
|
+
// PreToolUse/PostToolUse are **Bash-only** (Write/Edit/WebSearch/MCP
|
|
39
|
+
// calls do not trigger them). `precompact_digest` is unmapped —
|
|
40
|
+
// Codex has no PreCompact event; cclaw covers it via `/cc-ops retro`.
|
|
41
|
+
session_rehydrate: "SessionStart matcher startup|resume",
|
|
42
|
+
pre_tool_prompt_guard: "PreToolUse matcher Bash -> prompt-guard.sh (plus UserPromptSubmit for non-Bash prompts)",
|
|
43
|
+
pre_tool_workflow_guard: "PreToolUse matcher Bash -> workflow-guard.sh (Bash-only)",
|
|
44
|
+
post_tool_context_monitor: "PostToolUse matcher Bash -> context-monitor.sh (Bash-only)",
|
|
45
|
+
stop_checkpoint: "Stop -> stop-checkpoint.sh"
|
|
40
46
|
}
|
|
41
47
|
};
|
package/dist/content/hooks.js
CHANGED
|
@@ -1209,14 +1209,14 @@ Cclaw generates real hook integrations for every harness that exposes a
|
|
|
1209
1209
|
hook primitive:
|
|
1210
1210
|
- **Claude/Cursor:** lifecycle rehydration + PreToolUse/PostToolUse + Stop
|
|
1211
1211
|
- **OpenCode:** session lifecycle + system transform rehydration + bootstrap parity (digest/warnings/knowledge snapshot)
|
|
1212
|
-
- **Codex:**
|
|
1212
|
+
- **Codex:** Codex CLI ≥ v0.114 exposes lifecycle hooks at \`.codex/hooks.json\`, gated behind \`[features] codex_hooks = true\` in \`~/.codex/config.toml\`. \`PreToolUse\`/\`PostToolUse\` intercept **only the \`Bash\` tool** in Codex; \`Write\`/\`Edit\`/\`WebSearch\`/MCP calls are substituted via the \`/cc\` skill bodies under \`.agents/skills/cc*/SKILL.md\` and explicit in-turn agent steps. See \`.cclaw/references/harnesses/codex-playbook.md\` for the coverage matrix.
|
|
1213
1213
|
|
|
1214
1214
|
| Harness | Hook file | Events |
|
|
1215
1215
|
|---------|-----------|--------|
|
|
1216
1216
|
| Claude Code | \`.claude/hooks/hooks.json\` | SessionStart(startup/resume/clear/compact), PreToolUse, PostToolUse, Stop |
|
|
1217
1217
|
| Cursor | \`.cursor/hooks.json\` | sessionStart/sessionResume/sessionClear/sessionCompact, preToolUse, postToolUse, stop |
|
|
1218
1218
|
| OpenCode | \`${RUNTIME_ROOT}/hooks/opencode-plugin.mjs\` | session.created/updated/resumed/cleared/compacted/idle, tool.execute.before/after, system transform |
|
|
1219
|
-
| Codex |
|
|
1219
|
+
| Codex | \`.codex/hooks.json\` | SessionStart(startup/resume), UserPromptSubmit, PreToolUse(Bash), PostToolUse(Bash), Stop (feature-gated by \`codex_hooks = true\`) |
|
|
1220
1220
|
|
|
1221
1221
|
Hook state files:
|
|
1222
1222
|
- \`${RUNTIME_ROOT}/state/stage-activity.jsonl\`
|
|
@@ -23,4 +23,23 @@ export declare function summarizeObservationsScript(): string;
|
|
|
23
23
|
*/
|
|
24
24
|
export declare function claudeHooksJsonWithObservation(): string;
|
|
25
25
|
export declare function cursorHooksJsonWithObservation(): string;
|
|
26
|
+
/**
|
|
27
|
+
* Codex CLI ≥ v0.114 hooks. Differences vs. the Claude shape:
|
|
28
|
+
*
|
|
29
|
+
* - `SessionStart` matcher is limited to `startup|resume` — Codex does
|
|
30
|
+
* not emit `clear` or `compact` lifecycle phases.
|
|
31
|
+
* - `PreToolUse` / `PostToolUse` fire **only for the `Bash` tool**
|
|
32
|
+
* (documented Codex limitation, v0.114/v0.115). We use the `Bash`
|
|
33
|
+
* matcher verbatim so Codex doesn't silently swallow our commands.
|
|
34
|
+
* - `UserPromptSubmit` is supported and is the closest analogue to
|
|
35
|
+
* Cursor's `preToolUse` for non-Bash tooling — we run prompt-guard
|
|
36
|
+
* there so workflow/prompt checks still fire when the tool being
|
|
37
|
+
* used is `Write` or `Edit` rather than `Bash`.
|
|
38
|
+
* - There is no `PreCompact` event in Codex CLI — pre-compact
|
|
39
|
+
* semantics are carried by the agent itself inside `/cc-ops retro`.
|
|
40
|
+
*
|
|
41
|
+
* The entire file is inert unless the user opts into
|
|
42
|
+
* `[features] codex_hooks = true` in `~/.codex/config.toml`; cclaw
|
|
43
|
+
* doctor and the init prompt handle that flag.
|
|
44
|
+
*/
|
|
26
45
|
export declare function codexHooksJsonWithObservation(): string;
|
package/dist/content/observe.js
CHANGED
|
@@ -1792,20 +1792,44 @@ export function cursorHooksJsonWithObservation() {
|
|
|
1792
1792
|
}
|
|
1793
1793
|
}, null, 2);
|
|
1794
1794
|
}
|
|
1795
|
+
/**
|
|
1796
|
+
* Codex CLI ≥ v0.114 hooks. Differences vs. the Claude shape:
|
|
1797
|
+
*
|
|
1798
|
+
* - `SessionStart` matcher is limited to `startup|resume` — Codex does
|
|
1799
|
+
* not emit `clear` or `compact` lifecycle phases.
|
|
1800
|
+
* - `PreToolUse` / `PostToolUse` fire **only for the `Bash` tool**
|
|
1801
|
+
* (documented Codex limitation, v0.114/v0.115). We use the `Bash`
|
|
1802
|
+
* matcher verbatim so Codex doesn't silently swallow our commands.
|
|
1803
|
+
* - `UserPromptSubmit` is supported and is the closest analogue to
|
|
1804
|
+
* Cursor's `preToolUse` for non-Bash tooling — we run prompt-guard
|
|
1805
|
+
* there so workflow/prompt checks still fire when the tool being
|
|
1806
|
+
* used is `Write` or `Edit` rather than `Bash`.
|
|
1807
|
+
* - There is no `PreCompact` event in Codex CLI — pre-compact
|
|
1808
|
+
* semantics are carried by the agent itself inside `/cc-ops retro`.
|
|
1809
|
+
*
|
|
1810
|
+
* The entire file is inert unless the user opts into
|
|
1811
|
+
* `[features] codex_hooks = true` in `~/.codex/config.toml`; cclaw
|
|
1812
|
+
* doctor and the init prompt handle that flag.
|
|
1813
|
+
*/
|
|
1795
1814
|
export function codexHooksJsonWithObservation() {
|
|
1796
1815
|
return JSON.stringify({
|
|
1797
1816
|
cclawHookSchemaVersion: 1,
|
|
1798
1817
|
hooks: {
|
|
1799
1818
|
SessionStart: [{
|
|
1800
|
-
matcher: "startup|resume
|
|
1819
|
+
matcher: "startup|resume",
|
|
1801
1820
|
hooks: [{
|
|
1802
1821
|
type: "command",
|
|
1803
|
-
command: `bash ${RUNTIME_ROOT}/hooks/session-start.sh
|
|
1804
|
-
|
|
1822
|
+
command: `bash ${RUNTIME_ROOT}/hooks/session-start.sh`
|
|
1823
|
+
}]
|
|
1824
|
+
}],
|
|
1825
|
+
UserPromptSubmit: [{
|
|
1826
|
+
hooks: [{
|
|
1827
|
+
type: "command",
|
|
1828
|
+
command: `bash ${RUNTIME_ROOT}/hooks/prompt-guard.sh`
|
|
1805
1829
|
}]
|
|
1806
1830
|
}],
|
|
1807
1831
|
PreToolUse: [{
|
|
1808
|
-
matcher: "
|
|
1832
|
+
matcher: "Bash",
|
|
1809
1833
|
hooks: [{
|
|
1810
1834
|
type: "command",
|
|
1811
1835
|
command: `bash ${RUNTIME_ROOT}/hooks/prompt-guard.sh`
|
|
@@ -1815,7 +1839,7 @@ export function codexHooksJsonWithObservation() {
|
|
|
1815
1839
|
}]
|
|
1816
1840
|
}],
|
|
1817
1841
|
PostToolUse: [{
|
|
1818
|
-
matcher: "
|
|
1842
|
+
matcher: "Bash",
|
|
1819
1843
|
hooks: [{
|
|
1820
1844
|
type: "command",
|
|
1821
1845
|
command: `bash ${RUNTIME_ROOT}/hooks/context-monitor.sh`
|
|
@@ -1827,14 +1851,6 @@ export function codexHooksJsonWithObservation() {
|
|
|
1827
1851
|
command: `bash ${RUNTIME_ROOT}/hooks/stop-checkpoint.sh`,
|
|
1828
1852
|
timeout: 10
|
|
1829
1853
|
}]
|
|
1830
|
-
}],
|
|
1831
|
-
PreCompact: [{
|
|
1832
|
-
matcher: "manual|auto",
|
|
1833
|
-
hooks: [{
|
|
1834
|
-
type: "command",
|
|
1835
|
-
command: `bash ${RUNTIME_ROOT}/hooks/pre-compact.sh`,
|
|
1836
|
-
timeout: 10
|
|
1837
|
-
}]
|
|
1838
1854
|
}]
|
|
1839
1855
|
}
|
|
1840
1856
|
}, null, 2);
|
package/dist/doctor.js
CHANGED
|
@@ -20,6 +20,7 @@ import { reconcileAndWriteCurrentStageGateCatalog, verifyCompletedStagesGateClos
|
|
|
20
20
|
import { parseTddCycleLog, validateTddCycleOrder } from "./tdd-cycle.js";
|
|
21
21
|
import { stageSkillFolder } from "./content/skills.js";
|
|
22
22
|
import { doctorCheckMetadata } from "./doctor-registry.js";
|
|
23
|
+
import { classifyCodexHooksFlag, codexConfigPath, readCodexConfig } from "./codex-feature-flag.js";
|
|
23
24
|
import { LANGUAGE_RULE_PACK_DIR, LANGUAGE_RULE_PACK_FILES, LEGACY_LANGUAGE_RULE_PACK_FOLDERS, UTILITY_SKILL_FOLDERS } from "./content/utility-skills.js";
|
|
24
25
|
import { CONTEXT_MODES, DEFAULT_CONTEXT_MODE } from "./content/contexts.js";
|
|
25
26
|
import { DOCTOR_REFERENCE_MARKDOWN } from "./content/doctor-references.js";
|
|
@@ -635,16 +636,18 @@ export async function doctorChecks(projectRoot, options = {}) {
|
|
|
635
636
|
});
|
|
636
637
|
}
|
|
637
638
|
}
|
|
638
|
-
// Hook JSON files per harness.
|
|
639
|
-
//
|
|
640
|
-
//
|
|
639
|
+
// Hook JSON files per harness. OpenCode ships hooks through its plugin
|
|
640
|
+
// system (covered below). Codex joined the managed list in v0.40.0 — Codex
|
|
641
|
+
// CLI ≥ v0.114 consumes `.codex/hooks.json` behind the `codex_hooks`
|
|
642
|
+
// feature flag.
|
|
641
643
|
const hookPaths = {
|
|
642
644
|
claude: ".claude/hooks/hooks.json",
|
|
643
|
-
cursor: ".cursor/hooks.json"
|
|
645
|
+
cursor: ".cursor/hooks.json",
|
|
646
|
+
codex: ".codex/hooks.json"
|
|
644
647
|
};
|
|
645
648
|
for (const harness of configuredHarnesses) {
|
|
646
649
|
const hp = hookPaths[harness];
|
|
647
|
-
if (!hp && harness !== "opencode"
|
|
650
|
+
if (!hp && harness !== "opencode") {
|
|
648
651
|
checks.push({
|
|
649
652
|
name: `hook:json:${harness}`,
|
|
650
653
|
ok: false,
|
|
@@ -661,7 +664,7 @@ export async function doctorChecks(projectRoot, options = {}) {
|
|
|
661
664
|
ok: hookOk,
|
|
662
665
|
details: fullPath
|
|
663
666
|
});
|
|
664
|
-
if (harness === "claude" || harness === "cursor") {
|
|
667
|
+
if (harness === "claude" || harness === "cursor" || harness === "codex") {
|
|
665
668
|
const schema = validateHookDocument(harness, parsed);
|
|
666
669
|
checks.push({
|
|
667
670
|
name: `hook:schema:${harness}`,
|
|
@@ -762,10 +765,11 @@ export async function doctorChecks(projectRoot, options = {}) {
|
|
|
762
765
|
});
|
|
763
766
|
}
|
|
764
767
|
if (configuredHarnesses.includes("codex")) {
|
|
765
|
-
// Codex CLI has no
|
|
766
|
-
//
|
|
767
|
-
// skills under `.agents/skills/
|
|
768
|
-
//
|
|
768
|
+
// Codex CLI has no custom slash-command discovery (`.codex/commands/*`
|
|
769
|
+
// was never read, even historically). cclaw ships codex entry points
|
|
770
|
+
// as skills under `.agents/skills/cc*/SKILL.md`; Codex v0.114+ also
|
|
771
|
+
// supports lifecycle hooks at `.codex/hooks.json` (gated by the
|
|
772
|
+
// `codex_hooks` feature flag in `~/.codex/config.toml`).
|
|
769
773
|
const skillsRoot = path.join(projectRoot, ".agents/skills");
|
|
770
774
|
for (const skillName of harnessShimSkillNames()) {
|
|
771
775
|
const skillPath = path.join(skillsRoot, skillName, "SKILL.md");
|
|
@@ -791,26 +795,87 @@ export async function doctorChecks(projectRoot, options = {}) {
|
|
|
791
795
|
: `${skillPath} absent; cannot validate frontmatter`
|
|
792
796
|
});
|
|
793
797
|
}
|
|
794
|
-
//
|
|
795
|
-
//
|
|
796
|
-
//
|
|
798
|
+
// Hook wiring: the generated `.codex/hooks.json` must reference every
|
|
799
|
+
// runtime script cclaw needs. Separate from the schema check above;
|
|
800
|
+
// schema covers structure, this check covers semantic wiring.
|
|
801
|
+
const codexHooksFile = path.join(projectRoot, ".codex/hooks.json");
|
|
802
|
+
const codexDoc = await readHookDocument(codexHooksFile);
|
|
803
|
+
const codexHooks = toObject(codexDoc?.hooks) ?? {};
|
|
804
|
+
const codexSessionCmds = collectHookCommands(codexHooks.SessionStart);
|
|
805
|
+
const codexPreCmds = collectHookCommands(codexHooks.PreToolUse);
|
|
806
|
+
const codexPostCmds = collectHookCommands(codexHooks.PostToolUse);
|
|
807
|
+
const codexStopCmds = collectHookCommands(codexHooks.Stop);
|
|
808
|
+
const codexWiringOk = codexSessionCmds.some((cmd) => cmd.includes("session-start.sh")) &&
|
|
809
|
+
codexPreCmds.some((cmd) => cmd.includes("prompt-guard.sh")) &&
|
|
810
|
+
codexPreCmds.some((cmd) => cmd.includes("workflow-guard.sh")) &&
|
|
811
|
+
codexPostCmds.some((cmd) => cmd.includes("context-monitor.sh")) &&
|
|
812
|
+
codexStopCmds.some((cmd) => cmd.includes("stop-checkpoint.sh"));
|
|
813
|
+
checks.push({
|
|
814
|
+
name: "hook:wiring:codex",
|
|
815
|
+
ok: codexWiringOk,
|
|
816
|
+
details: `${codexHooksFile} must wire session-start/prompt-guard/workflow-guard/context-monitor/stop-checkpoint (PreToolUse/PostToolUse run Bash-only in Codex v0.114+)`
|
|
817
|
+
});
|
|
818
|
+
// Feature flag warning: Codex ignores `.codex/hooks.json` unless the
|
|
819
|
+
// user has `[features] codex_hooks = true` in `~/.codex/config.toml`.
|
|
820
|
+
// Advisory warning — not a hard failure, because the skills still
|
|
821
|
+
// work without the flag.
|
|
822
|
+
const codexConfig = codexConfigPath();
|
|
823
|
+
let featureFlagNote = "";
|
|
824
|
+
try {
|
|
825
|
+
const content = await readCodexConfig(codexConfig);
|
|
826
|
+
const state = classifyCodexHooksFlag(content);
|
|
827
|
+
featureFlagNote =
|
|
828
|
+
state === "enabled"
|
|
829
|
+
? `codex_hooks feature flag is enabled in ${codexConfig}`
|
|
830
|
+
: state === "missing-file"
|
|
831
|
+
? `warning: ${codexConfig} does not exist; .codex/hooks.json will be ignored until you create it with \`[features]\\ncodex_hooks = true\\n\`.`
|
|
832
|
+
: state === "missing-section"
|
|
833
|
+
? `warning: ${codexConfig} has no [features] section; add \`[features]\\ncodex_hooks = true\\n\` to enable cclaw hooks.`
|
|
834
|
+
: state === "missing-key"
|
|
835
|
+
? `warning: ${codexConfig} is missing the codex_hooks key under [features]. Add \`codex_hooks = true\` to enable cclaw hooks.`
|
|
836
|
+
: `warning: ${codexConfig} sets codex_hooks to a non-true value; set \`codex_hooks = true\` under [features] to enable cclaw hooks.`;
|
|
837
|
+
}
|
|
838
|
+
catch (err) {
|
|
839
|
+
featureFlagNote = `warning: could not read ${codexConfig}: ${err instanceof Error ? err.message : String(err)}`;
|
|
840
|
+
}
|
|
841
|
+
checks.push({
|
|
842
|
+
name: "warning:codex:feature_flag",
|
|
843
|
+
ok: true,
|
|
844
|
+
details: featureFlagNote
|
|
845
|
+
});
|
|
846
|
+
// Legacy `.codex/commands/*` must not linger from older cclaw installs.
|
|
847
|
+
// (The `.codex/hooks.json` path is now managed and is validated above,
|
|
848
|
+
// so there is no longer a legacy_hooks_json warning.)
|
|
797
849
|
const legacyCommandsDir = path.join(projectRoot, ".codex/commands");
|
|
798
850
|
const legacyCommandsPresent = await exists(legacyCommandsDir);
|
|
799
851
|
checks.push({
|
|
800
852
|
name: "warning:codex:legacy_commands_dir",
|
|
801
853
|
ok: true,
|
|
802
854
|
details: legacyCommandsPresent
|
|
803
|
-
? `warning: ${legacyCommandsDir} still present; Codex never
|
|
855
|
+
? `warning: ${legacyCommandsDir} still present; Codex never consumed this directory — run \`cclaw sync\` to remove it.`
|
|
804
856
|
: `no legacy ${legacyCommandsDir} detected`
|
|
805
857
|
});
|
|
806
|
-
|
|
807
|
-
|
|
858
|
+
// Legacy v0.39.x skill layout under `.agents/skills/cclaw-cc*/`
|
|
859
|
+
// must have been removed — cclaw sync deletes these automatically,
|
|
860
|
+
// but flag leftovers so users notice an upgrade issue.
|
|
861
|
+
const legacyCodexSkills = [];
|
|
862
|
+
try {
|
|
863
|
+
const entries = await fs.readdir(skillsRoot);
|
|
864
|
+
for (const entry of entries) {
|
|
865
|
+
if (/^cclaw-cc(?:-.*)?$/u.test(entry)) {
|
|
866
|
+
legacyCodexSkills.push(entry);
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
catch {
|
|
871
|
+
// skills root absent; nothing to warn about
|
|
872
|
+
}
|
|
808
873
|
checks.push({
|
|
809
|
-
name: "warning:codex:
|
|
810
|
-
ok:
|
|
811
|
-
details:
|
|
812
|
-
? `
|
|
813
|
-
: `
|
|
874
|
+
name: "warning:codex:legacy_cclaw_cc_skills",
|
|
875
|
+
ok: legacyCodexSkills.length === 0,
|
|
876
|
+
details: legacyCodexSkills.length === 0
|
|
877
|
+
? `no legacy cclaw-cc* skill folders detected under .agents/skills/`
|
|
878
|
+
: `warning: legacy skill folders from cclaw v0.39.x present (${legacyCodexSkills.join(", ")}); run \`cclaw sync\` to remove them.`
|
|
814
879
|
});
|
|
815
880
|
}
|
|
816
881
|
if (configuredHarnesses.includes("opencode")) {
|
package/dist/harness-adapters.js
CHANGED
|
@@ -14,35 +14,48 @@ const RUNTIME_AGENTS_BLOCK_GLOBAL_PATTERN = new RegExp(RUNTIME_AGENTS_BLOCK_SOUR
|
|
|
14
14
|
const UTILITY_SHIMS = [
|
|
15
15
|
{
|
|
16
16
|
fileName: "cc-next.md",
|
|
17
|
-
skillName: "
|
|
17
|
+
skillName: "cc-next",
|
|
18
18
|
command: "next",
|
|
19
19
|
skillFolder: "flow-next-step",
|
|
20
20
|
commandFile: "next.md"
|
|
21
21
|
},
|
|
22
22
|
{
|
|
23
23
|
fileName: "cc-ideate.md",
|
|
24
|
-
skillName: "
|
|
24
|
+
skillName: "cc-ideate",
|
|
25
25
|
command: "ideate",
|
|
26
26
|
skillFolder: "flow-ideate",
|
|
27
27
|
commandFile: "ideate.md"
|
|
28
28
|
},
|
|
29
29
|
{
|
|
30
30
|
fileName: "cc-view.md",
|
|
31
|
-
skillName: "
|
|
31
|
+
skillName: "cc-view",
|
|
32
32
|
command: "view",
|
|
33
33
|
skillFolder: "flow-view",
|
|
34
34
|
commandFile: "view.md"
|
|
35
35
|
},
|
|
36
36
|
{
|
|
37
37
|
fileName: "cc-ops.md",
|
|
38
|
-
skillName: "
|
|
38
|
+
skillName: "cc-ops",
|
|
39
39
|
command: "ops",
|
|
40
40
|
skillFolder: "flow-ops",
|
|
41
41
|
commandFile: "ops.md"
|
|
42
42
|
}
|
|
43
43
|
];
|
|
44
44
|
/** Skill-kind shim name for the root `/cc` entry point. */
|
|
45
|
-
const ENTRY_SHIM_SKILL_NAME = "
|
|
45
|
+
const ENTRY_SHIM_SKILL_NAME = "cc";
|
|
46
|
+
/**
|
|
47
|
+
* Skill directory names that v0.39.0 / v0.39.1 installed under
|
|
48
|
+
* `.agents/skills/` before the rename. We delete these on every sync so
|
|
49
|
+
* upgrades from those versions do not leave orphaned `cclaw-cc*`
|
|
50
|
+
* folders that would double-register in Codex's skill listing.
|
|
51
|
+
*/
|
|
52
|
+
const LEGACY_CODEX_SKILL_NAMES = [
|
|
53
|
+
"cclaw-cc",
|
|
54
|
+
"cclaw-cc-next",
|
|
55
|
+
"cclaw-cc-view",
|
|
56
|
+
"cclaw-cc-ops",
|
|
57
|
+
"cclaw-cc-ideate"
|
|
58
|
+
];
|
|
46
59
|
/**
|
|
47
60
|
* Shims that older cclaw versions installed as top-level slash commands but
|
|
48
61
|
* which we now treat as internal (skill-only, invoked by the agent, never
|
|
@@ -98,16 +111,20 @@ export const HARNESS_ADAPTERS = {
|
|
|
98
111
|
codex: {
|
|
99
112
|
id: "codex",
|
|
100
113
|
// Codex CLI reads skills from the universal `.agents/skills/` path
|
|
101
|
-
// (OpenAI Codex 0.89, Jan 2026
|
|
102
|
-
//
|
|
103
|
-
//
|
|
104
|
-
//
|
|
105
|
-
//
|
|
114
|
+
// (OpenAI Codex 0.89, Jan 2026). It does NOT have a native
|
|
115
|
+
// `.codex/commands/*` slash-command discovery — cclaw installs
|
|
116
|
+
// its entry points as skills here. Since v0.114 (Mar 2026) Codex
|
|
117
|
+
// also exposes lifecycle hooks via `.codex/hooks.json`, behind
|
|
118
|
+
// the `[features] codex_hooks = true` feature flag in
|
|
119
|
+
// `~/.codex/config.toml`. cclaw writes that file on sync and
|
|
120
|
+
// `hookSurface: "limited"` records the reality: SessionStart /
|
|
121
|
+
// UserPromptSubmit / Stop fire for every turn, but PreToolUse /
|
|
122
|
+
// PostToolUse only intercept the `Bash` tool.
|
|
106
123
|
commandDir: ".agents/skills",
|
|
107
124
|
shimKind: "skill",
|
|
108
125
|
capabilities: {
|
|
109
126
|
nativeSubagentDispatch: "none",
|
|
110
|
-
hookSurface: "
|
|
127
|
+
hookSurface: "limited",
|
|
111
128
|
structuredAsk: "plain-text",
|
|
112
129
|
subagentFallback: "role-switch"
|
|
113
130
|
}
|
|
@@ -122,6 +139,7 @@ export function harnessTier(harnessId) {
|
|
|
122
139
|
}
|
|
123
140
|
if (capabilities.hookSurface === "full" ||
|
|
124
141
|
capabilities.hookSurface === "plugin" ||
|
|
142
|
+
capabilities.hookSurface === "limited" ||
|
|
125
143
|
capabilities.nativeSubagentDispatch === "generic" ||
|
|
126
144
|
capabilities.nativeSubagentDispatch === "partial") {
|
|
127
145
|
return "tier2";
|
|
@@ -209,26 +227,33 @@ If the same approach fails three times in a row (same command, same finding, sam
|
|
|
209
227
|
### Detail Level
|
|
210
228
|
|
|
211
229
|
- This managed AGENTS block is intentionally minimal for cross-project use.
|
|
212
|
-
- Harness coverage is tiered: Tier1 (claude), Tier2 (cursor/opencode/codex), Tier3 (fallback/manual-only).
|
|
230
|
+
- Harness coverage is tiered: Tier1 (claude), Tier2 (cursor/opencode/codex — codex has Bash-only tool hooks), Tier3 (fallback/manual-only).
|
|
213
231
|
- Detailed operating procedures live in \`.cclaw/skills/using-cclaw/SKILL.md\`.
|
|
214
232
|
- Preamble budget and cooldown rules live in \`.cclaw/references/protocols/ethos.md\`.
|
|
215
233
|
- Subagent orchestration patterns: \`.cclaw/skills/subagent-dev/SKILL.md\` and \`.cclaw/skills/parallel-dispatch/SKILL.md\`.
|
|
216
234
|
|
|
217
235
|
### Codex users
|
|
218
236
|
|
|
219
|
-
OpenAI Codex CLI has **no native \`/cc\` slash command**
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
237
|
+
OpenAI Codex CLI has **no native \`/cc\` slash command** (custom prompts
|
|
238
|
+
were deprecated in v0.89, Jan 2026). The \`/cc\`, \`/cc-next\`,
|
|
239
|
+
\`/cc-ideate\`, \`/cc-view\`, \`/cc-ops\` tokens above describe intent — in
|
|
240
|
+
Codex they map onto skills cclaw installs at
|
|
241
|
+
\`.agents/skills/cc*/SKILL.md\`. Activate one of two ways:
|
|
223
242
|
|
|
224
|
-
- Type \`/use
|
|
243
|
+
- Type \`/use cc\` (or \`cc-next\`, etc.) at Codex's prompt.
|
|
225
244
|
- Type \`/cc …\` as plain text — Codex matches the skill \`description\`
|
|
226
245
|
frontmatter (which spells out the token verbatim) and loads the right
|
|
227
246
|
skill body automatically.
|
|
228
247
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
\`.cclaw/
|
|
248
|
+
Codex CLI v0.114+ (Mar 2026) **does** expose lifecycle hooks via
|
|
249
|
+
\`.codex/hooks.json\`, gated by the \`[features] codex_hooks = true\` flag
|
|
250
|
+
in \`~/.codex/config.toml\`. cclaw generates \`.codex/hooks.json\` on
|
|
251
|
+
sync; if the feature flag is off, hooks are inert and cclaw's
|
|
252
|
+
session-start rehydration simply does not fire. Run \`cclaw doctor\` to
|
|
253
|
+
see if the flag is missing. \`.codex/commands/*\` is still unused by
|
|
254
|
+
Codex CLI and is removed on every sync. See
|
|
255
|
+
\`.cclaw/references/harnesses/codex-playbook.md\` for the hook coverage
|
|
256
|
+
matrix (Bash-only \`PreToolUse\`/\`PostToolUse\`; other events are full).
|
|
232
257
|
${CCLAW_MARKER_END}`;
|
|
233
258
|
}
|
|
234
259
|
/** Removes the cclaw AGENTS.md block. */
|
|
@@ -336,14 +361,22 @@ function codexSkillBody(command, skillFolder, commandFile) {
|
|
|
336
361
|
const extraContractHeading = command === "cc"
|
|
337
362
|
? "If you have not already loaded the cclaw meta-skill this session, also load `.cclaw/skills/using-cclaw/SKILL.md` — it is the routing brain for stage/utility selection."
|
|
338
363
|
: "This skill is a utility entry point, not a flow stage. Do not mutate `.cclaw/state/flow-state.json` directly.";
|
|
364
|
+
const skillSlug = command === "cc" ? "cc" : `cc-${command}`;
|
|
339
365
|
return `# ${title}
|
|
340
366
|
|
|
341
367
|
You are running inside the OpenAI Codex harness. Codex has **no native
|
|
342
|
-
\`${slashToken}\` slash command
|
|
343
|
-
ships its entry points as skills
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
368
|
+
\`${slashToken}\` slash command** — custom prompts were deprecated in
|
|
369
|
+
Codex CLI v0.89 (Jan 2026). cclaw ships its entry points as skills
|
|
370
|
+
under \`.agents/skills/${skillSlug}/\` so the user can either:
|
|
371
|
+
|
|
372
|
+
- Type \`/use ${skillSlug}\` at the Codex prompt, or
|
|
373
|
+
- Type \`${slashToken} …\` (or describe the intent in English) — Codex's
|
|
374
|
+
skill matcher picks this skill up via the description frontmatter.
|
|
375
|
+
|
|
376
|
+
Lifecycle hooks **are** available in Codex CLI v0.114+ (behind the
|
|
377
|
+
\`[features] codex_hooks = true\` flag in \`~/.codex/config.toml\`) and
|
|
378
|
+
cclaw installs a matching \`.codex/hooks.json\` — see the playbook for
|
|
379
|
+
what the hook surface does and does not cover.
|
|
347
380
|
|
|
348
381
|
## Protocol
|
|
349
382
|
|
|
@@ -363,10 +396,11 @@ follow the steps below.
|
|
|
363
396
|
append a completed row with \`evidenceRefs\` to
|
|
364
397
|
\`.cclaw/state/delegation-log.json\`. Silent auto-waiver is disabled
|
|
365
398
|
(v0.33+).
|
|
366
|
-
- Codex
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
for
|
|
399
|
+
- Codex's \`PreToolUse\` / \`PostToolUse\` hooks currently only intercept
|
|
400
|
+
the \`Bash\` tool. \`Write\`, \`Edit\`, \`WebSearch\`, and MCP tool calls
|
|
401
|
+
are **not** gated by hooks — read
|
|
402
|
+
\`.cclaw/references/harnesses/codex-playbook.md\` for what cclaw
|
|
403
|
+
substitutes with in-turn agent steps for those call classes.
|
|
370
404
|
`;
|
|
371
405
|
}
|
|
372
406
|
function codexSkillMarkdown(command, skillName, skillFolder, commandFile) {
|
|
@@ -406,9 +440,16 @@ async function writeSkillKindShims(commandDir) {
|
|
|
406
440
|
}
|
|
407
441
|
/**
|
|
408
442
|
* Legacy codex surfaces cclaw wrote before v0.39.0 that Codex CLI never
|
|
409
|
-
*
|
|
410
|
-
*
|
|
411
|
-
*
|
|
443
|
+
* consumed (`.codex/commands/*.md` had no discovery primitive). We keep
|
|
444
|
+
* removing `.codex/commands/` on every sync so upgrades from those
|
|
445
|
+
* installs leave a clean slate, but as of v0.40.0 we DO write
|
|
446
|
+
* `.codex/hooks.json` again — Codex CLI grew a real hooks API in
|
|
447
|
+
* v0.114.0 (Mar 2026), and that file is the current, supported target.
|
|
448
|
+
*
|
|
449
|
+
* This function also removes skill folders named after the old
|
|
450
|
+
* `cclaw-cc*` scheme (v0.39.0 / v0.39.1) now that cclaw installs them
|
|
451
|
+
* as plain `cc*`. Leaving them around would make Codex list two skills
|
|
452
|
+
* for the same entry point.
|
|
412
453
|
*/
|
|
413
454
|
async function cleanupLegacyCodexSurfaces(projectRoot) {
|
|
414
455
|
const legacyCommandsDir = path.join(projectRoot, ".codex/commands");
|
|
@@ -418,16 +459,21 @@ async function cleanupLegacyCodexSurfaces(projectRoot) {
|
|
|
418
459
|
catch {
|
|
419
460
|
// best-effort cleanup
|
|
420
461
|
}
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
462
|
+
// Remove the old `cclaw-cc*` skill folders if they exist from a
|
|
463
|
+
// previous cclaw install. Idempotent; best-effort.
|
|
464
|
+
const legacySkillsRoot = path.join(projectRoot, ".agents/skills");
|
|
465
|
+
for (const name of LEGACY_CODEX_SKILL_NAMES) {
|
|
466
|
+
const folder = path.join(legacySkillsRoot, name);
|
|
467
|
+
try {
|
|
468
|
+
await fs.rm(folder, { recursive: true, force: true });
|
|
469
|
+
}
|
|
470
|
+
catch {
|
|
471
|
+
// best-effort
|
|
472
|
+
}
|
|
427
473
|
}
|
|
428
|
-
// If `.codex/` is now empty we drop it
|
|
429
|
-
//
|
|
430
|
-
//
|
|
474
|
+
// If `.codex/` is now empty we drop it — happens when neither hooks
|
|
475
|
+
// are enabled nor the user has their own state there. Otherwise we
|
|
476
|
+
// leave the directory alone.
|
|
431
477
|
try {
|
|
432
478
|
const codexDir = path.join(projectRoot, ".codex");
|
|
433
479
|
const entries = await fs.readdir(codexDir);
|
package/dist/install.js
CHANGED
|
@@ -23,7 +23,7 @@ import { archiveCommandContract, archiveCommandSkillMarkdown } from "./content/a
|
|
|
23
23
|
import { rewindCommandContract, rewindCommandSkillMarkdown } from "./content/rewind-command.js";
|
|
24
24
|
import { subagentDrivenDevSkill, parallelAgentsSkill } from "./content/subagents.js";
|
|
25
25
|
import { sessionHooksSkillMarkdown } from "./content/session-hooks.js";
|
|
26
|
-
import { sessionStartScript, stopCheckpointScript, preCompactScript, opencodePluginJs, claudeHooksJson, cursorHooksJson } from "./content/hooks.js";
|
|
26
|
+
import { sessionStartScript, stopCheckpointScript, preCompactScript, opencodePluginJs, claudeHooksJson, codexHooksJson, cursorHooksJson } from "./content/hooks.js";
|
|
27
27
|
import { contextMonitorScript, promptGuardScript, workflowGuardScript } from "./content/observe.js";
|
|
28
28
|
import { META_SKILL_NAME, usingCclawSkillMarkdown } from "./content/meta-skill.js";
|
|
29
29
|
import { decisionProtocolMarkdown, completionProtocolMarkdown, ethosProtocolMarkdown } from "./content/protocols.js";
|
|
@@ -667,10 +667,18 @@ async function writeHooks(projectRoot, config) {
|
|
|
667
667
|
await ensureDir(cursorDir);
|
|
668
668
|
await writeMergedHookJson(projectRoot, path.join(cursorDir, "hooks.json"), cursorHooksJson());
|
|
669
669
|
}
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
670
|
+
else if (harness === "codex") {
|
|
671
|
+
// Codex CLI ≥ v0.114 (Mar 2026) supports lifecycle hooks at
|
|
672
|
+
// `.codex/hooks.json`, gated behind the `[features] codex_hooks = true`
|
|
673
|
+
// flag in `~/.codex/config.toml`. cclaw always writes the file so
|
|
674
|
+
// the moment the flag flips on, the cclaw hooks start firing. See
|
|
675
|
+
// `codexHooksJsonWithObservation` for the Bash-only caveat on
|
|
676
|
+
// PreToolUse/PostToolUse. `cclaw doctor` warns if the feature flag
|
|
677
|
+
// is not set.
|
|
678
|
+
const codexDir = path.join(projectRoot, ".codex");
|
|
679
|
+
await ensureDir(codexDir);
|
|
680
|
+
await writeMergedHookJson(projectRoot, path.join(codexDir, "hooks.json"), codexHooksJson());
|
|
681
|
+
}
|
|
674
682
|
// OpenCode registration is auto-managed via opencode.json/opencode.jsonc.
|
|
675
683
|
}
|
|
676
684
|
}
|
|
@@ -905,13 +913,14 @@ async function writeCursorWorkflowRule(projectRoot, harnesses) {
|
|
|
905
913
|
}
|
|
906
914
|
async function syncDisabledHarnessArtifacts(projectRoot, harnesses) {
|
|
907
915
|
const enabled = new Set(harnesses);
|
|
908
|
-
//
|
|
909
|
-
//
|
|
910
|
-
//
|
|
911
|
-
//
|
|
916
|
+
// v0.40.0: `.codex/hooks.json` is back on the managed list now that
|
|
917
|
+
// Codex CLI actually consumes it (v0.114+, Mar 2026). Legacy
|
|
918
|
+
// `.codex/commands/` cleanup still happens unconditionally from
|
|
919
|
+
// `cleanupLegacyCodexSurfaces` inside `syncHarnessShims`.
|
|
912
920
|
const managedHookFiles = [
|
|
913
921
|
{ harness: "claude", hookPath: path.join(projectRoot, ".claude/hooks/hooks.json") },
|
|
914
|
-
{ harness: "cursor", hookPath: path.join(projectRoot, ".cursor/hooks.json") }
|
|
922
|
+
{ harness: "cursor", hookPath: path.join(projectRoot, ".cursor/hooks.json") },
|
|
923
|
+
{ harness: "codex", hookPath: path.join(projectRoot, ".codex/hooks.json") }
|
|
915
924
|
];
|
|
916
925
|
for (const entry of managedHookFiles) {
|
|
917
926
|
if (enabled.has(entry.harness))
|
|
@@ -1306,15 +1315,19 @@ export async function uninstallCclaw(projectRoot) {
|
|
|
1306
1315
|
// directory not present
|
|
1307
1316
|
}
|
|
1308
1317
|
}
|
|
1309
|
-
//
|
|
1310
|
-
//
|
|
1311
|
-
//
|
|
1312
|
-
// `.agents/skills
|
|
1318
|
+
// Codex shim location history:
|
|
1319
|
+
// - < v0.39.0: `.codex/commands/cc*.md` (never consumed by Codex CLI)
|
|
1320
|
+
// - v0.39.0 / v0.39.1: `.agents/skills/cclaw-cc*/SKILL.md`
|
|
1321
|
+
// - ≥ v0.40.0: `.agents/skills/cc*/SKILL.md` (matches Codex's `/use cc`
|
|
1322
|
+
// prompt verbatim)
|
|
1323
|
+
// Remove all three legacy layouts on uninstall so orphans can't linger.
|
|
1324
|
+
// We only touch cclaw-owned folder names — other tools share
|
|
1325
|
+
// `.agents/skills/` with us.
|
|
1313
1326
|
const codexSkillsRoot = path.join(projectRoot, ".agents/skills");
|
|
1314
1327
|
try {
|
|
1315
1328
|
const entries = await fs.readdir(codexSkillsRoot);
|
|
1316
1329
|
for (const entry of entries) {
|
|
1317
|
-
if (/^cclaw-(?:
|
|
1330
|
+
if (/^(?:cclaw-)?cc(?:-(?:next|view|ops|ideate))?$/u.test(entry)) {
|
|
1318
1331
|
await fs.rm(path.join(codexSkillsRoot, entry), { recursive: true, force: true });
|
|
1319
1332
|
}
|
|
1320
1333
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cclaw-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.40.0",
|
|
4
4
|
"description": "Installer-first flow toolkit for coding agents",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
},
|
|
18
18
|
"scripts": {
|
|
19
19
|
"clean:dist": "node -e \"import('node:fs/promises').then((fs) => fs.rm('dist', { recursive: true, force: true }))\"",
|
|
20
|
-
"build": "npm run clean:dist && tsc -p tsconfig.json",
|
|
20
|
+
"build": "npm run clean:dist && tsc -p tsconfig.json && node scripts/chmod-bin.mjs",
|
|
21
21
|
"test": "vitest run",
|
|
22
22
|
"test:watch": "vitest",
|
|
23
23
|
"test:coverage": "vitest run --coverage",
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"lint:hooks": "npm run build && node scripts/lint-generated-hooks.mjs",
|
|
26
26
|
"build:harness-docs": "npm run build && node scripts/build-harness-docs.mjs",
|
|
27
27
|
"build:plugin-manifests": "npm run build && node scripts/build-plugin-manifests.mjs",
|
|
28
|
-
"release:check": "npm run build && npm run test && node scripts/lint-generated-hooks.mjs && node scripts/build-plugin-manifests.mjs && npm pack --dry-run && node scripts/smoke-init.mjs",
|
|
28
|
+
"release:check": "npm run build && node scripts/verify-bin-executable.mjs && npm run test && node scripts/lint-generated-hooks.mjs && node scripts/build-plugin-manifests.mjs && npm pack --dry-run && node scripts/smoke-init.mjs",
|
|
29
29
|
"release:bundle": "npm run release:check && npm pack"
|
|
30
30
|
},
|
|
31
31
|
"keywords": [
|