@haposoft/cafekit 0.18.2 → 0.18.4
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 +1 -1
- package/bin/__tests__/adv-skill.test.js +370 -0
- package/bin/__tests__/agent-authority.test.js +38 -0
- package/bin/__tests__/agent-instructions.test.js +54 -0
- package/bin/__tests__/claude-plugin-build.test.js +151 -0
- package/bin/__tests__/codex-hook-runtime-fixture.cjs +26 -0
- package/bin/__tests__/codex-hooks-ownership.test.js +8 -11
- package/bin/__tests__/codex-hooks.test.js +87 -85
- package/bin/__tests__/codex-implicit-policy.test.js +160 -0
- package/bin/__tests__/codex-native-hook-boundaries.test.js +2 -4
- package/bin/__tests__/codex-native.test.js +136 -2298
- package/bin/__tests__/codex-plugin-build.test.js +230 -0
- package/bin/__tests__/codex-plugin-context.test.js +135 -0
- package/bin/__tests__/codex-plugin-hooks.test.js +220 -0
- package/bin/__tests__/codex-quiet-hooks.test.js +2 -8
- package/bin/__tests__/codex-runtime-layout.test.js +81 -0
- package/bin/__tests__/codex-setup-agents.test.js +216 -0
- package/bin/__tests__/codex-skill-projections.test.js +101 -0
- package/bin/__tests__/develop-contract.test.js +100 -874
- package/bin/__tests__/legacy-hooks-retired.test.js +63 -13
- package/bin/__tests__/migrate-apply.test.js +373 -0
- package/bin/__tests__/migrate-cli.test.js +152 -0
- package/bin/__tests__/migrate-merged.test.js +269 -0
- package/bin/__tests__/migrate-plan.test.js +271 -0
- package/bin/__tests__/omp-bridge.test.js +81 -5
- package/bin/__tests__/omp-hooks.test.js +24 -0
- package/bin/__tests__/orca-session.test.js +2 -16
- package/bin/__tests__/package-inventory.test.js +228 -2044
- package/bin/__tests__/plugin-safety-keys.test.js +121 -0
- package/bin/__tests__/privacy-paths.test.js +184 -0
- package/bin/__tests__/secret-scanner.test.js +73 -0
- package/bin/__tests__/skill-routing-source.test.js +6 -0
- package/bin/__tests__/toaa-skill.test.js +195 -0
- package/bin/__tests__/usage-hook-retired.test.js +24 -6
- package/bin/__tests__/validator-grounding.test.js +0 -198
- package/bin/__tests__/vibe-run.test.js +215 -0
- package/bin/__tests__/vibe-skill.test.js +548 -0
- package/bin/__tests__/website-docs.test.js +58 -0
- package/bin/install.js +99 -1
- package/bin/lib/backup.js +43 -24
- package/bin/lib/codex-install.js +12 -3
- package/bin/lib/context.js +5 -0
- package/bin/lib/lock.js +98 -18
- package/bin/lib/managed-writer.js +16 -10
- package/bin/lib/migrate-apply.js +202 -0
- package/bin/lib/migrate-merged.js +342 -0
- package/bin/lib/migrate-plan.js +419 -0
- package/bin/lib/plugin-inventory.js +33 -0
- package/bin/phases/claude-runtime.js +5 -3
- package/bin/phases/codex-hooks.js +38 -0
- package/bin/phases/codex-runtime.js +106 -74
- package/bin/phases/copy-payload.js +49 -1
- package/bin/phases/omp-runtime.js +4 -26
- package/bin/phases/root-config.js +5 -2
- package/package.json +1 -1
- package/src/claude/CLAUDE.md +1 -1
- package/src/claude/agents/advisor.md +55 -0
- package/src/claude/agents/debugger.md +2 -3
- package/src/claude/agents/deployer.md +2 -1
- package/src/claude/agents/docs-keeper.md +12 -0
- package/src/claude/agents/inspector.md +1 -1
- package/src/claude/agents/project-manager.md +2 -2
- package/src/claude/agents/researcher.md +0 -1
- package/src/claude/agents/spec-maker.md +4 -1
- package/src/claude/hooks/__tests__/plugin-context.test.js +96 -0
- package/src/claude/hooks/__tests__/plugin-options.test.js +141 -0
- package/src/claude/hooks/__tests__/plugin-state-isolation.test.js +125 -0
- package/src/claude/hooks/__tests__/precompact.test.js +7 -1
- package/src/claude/hooks/__tests__/privacy-block.test.js +33 -35
- package/src/claude/hooks/__tests__/setup-statusline.test.js +179 -0
- package/src/claude/hooks/__tests__/spec-gate.test.js +86 -48
- package/src/claude/hooks/__tests__/state.test.js +21 -0
- package/src/claude/hooks/agent.cjs +5 -2
- package/src/claude/hooks/inspect-block.cjs +3 -1
- package/src/claude/hooks/lib/hook-state-dir.cjs +19 -1
- package/src/claude/hooks/lib/plugin-options.cjs +47 -0
- package/src/claude/hooks/lib/privacy-command-analysis.cjs +130 -11
- package/src/claude/hooks/lib/privacy-paths.cjs +370 -10
- package/src/claude/hooks/lib/spec-touch.cjs +14 -1
- package/src/claude/hooks/plugin-context.cjs +80 -0
- package/src/claude/hooks/privacy-block.cjs +2 -1
- package/src/claude/hooks/rules.cjs +5 -2
- package/src/claude/hooks/session.cjs +4 -1
- package/src/claude/hooks/spec-state.cjs +5 -4
- package/src/claude/hooks/state.cjs +27 -1
- package/src/claude/migration-manifest.json +7 -1
- package/src/claude/plugin/commands/setup-statusline.md +12 -0
- package/src/claude/plugin/core.md +71 -0
- package/src/claude/rules/manage-docs.md +1 -2
- package/src/claude/rules/review-audit-self-decision.md +1 -1
- package/src/claude/rules/skill-workflow-routing.md +1 -1
- package/src/claude/rules/state-sync.md +14 -0
- package/src/claude/rules/workflow.md +1 -1
- package/src/claude/scripts/generate-skill-catalog.cjs +5 -0
- package/src/claude/scripts/provenance.cjs +40 -16
- package/src/claude/scripts/scan-staged-secrets.cjs +49 -3
- package/src/claude/scripts/setup-statusline.cjs +88 -0
- package/src/claude/skills/adv/SKILL.md +139 -0
- package/src/claude/skills/adv/references/optional-modes.md +41 -0
- package/src/claude/skills/adv/references/ultra-mode.md +29 -0
- package/src/claude/skills/brainstorm/SKILL.md +1 -1
- package/src/claude/skills/code-review/SKILL.md +4 -1
- package/src/claude/skills/code-review/references/pr-fix-reply.md +42 -0
- package/src/claude/skills/develop/SKILL.md +11 -3
- package/src/claude/skills/develop/references/quality-gate.md +1 -1
- package/src/claude/skills/fix/SKILL.md +2 -2
- package/src/claude/skills/git/SKILL.md +4 -2
- package/src/claude/skills/git/references/merge-pr.md +125 -0
- package/src/claude/skills/specs/SKILL.md +2 -2
- package/src/claude/skills/specs/references/review.md +1 -1
- package/src/claude/skills/specs/references/templates.md +2 -0
- package/src/claude/skills/sync/SKILL.md +4 -0
- package/src/claude/skills/toaa/SKILL.md +82 -0
- package/src/claude/skills/toaa/references/autonomous-orchestration.md +54 -0
- package/src/claude/skills/toaa/references/prerequisites-checklist.md +37 -0
- package/src/claude/skills/vibe/SKILL.md +69 -0
- package/src/claude/skills/vibe/references/pipeline-skill-integration.md +34 -0
- package/src/claude/skills/vibe/references/report-templates.md +84 -0
- package/src/claude/skills/vibe/references/subagent-orchestration.md +23 -0
- package/src/codex/AGENTS.md +1 -1
- package/src/codex/hooks/inspect-block.cjs +7 -4
- package/src/codex/hooks/lib/hook-context.cjs +119 -4
- package/src/codex/hooks/lib/plugin-options.cjs +2 -0
- package/src/codex/hooks/lib/privacy-state.cjs +18 -1
- package/src/codex/hooks/lib/state-store.cjs +31 -6
- package/src/codex/hooks/precompact.cjs +9 -8
- package/src/codex/hooks/privacy-approval.cjs +2 -2
- package/src/codex/hooks/privacy-block.cjs +5 -4
- package/src/codex/hooks/rules.cjs +12 -6
- package/src/codex/hooks/session.cjs +22 -10
- package/src/codex/plugin/core.md +76 -0
- package/src/codex/plugin/hooks/plugin-context.cjs +63 -0
- package/src/codex/plugin/skills/setup-agents/SKILL.md +17 -0
- package/src/codex/scripts/setup-agents.cjs +124 -0
- package/src/common/AGENTS.md +1 -1
- package/src/omp/extensions/{cafekit-bridge.mjs → cafekit-bridge.js} +73 -5
- package/src/omp/hooks/privacy-block.cjs +2 -1
- package/bin/__tests__/codex-research-projection.test.js +0 -70
- package/bin/__tests__/codex-specs-projection.test.js +0 -76
- package/bin/__tests__/specs-v2-execution-closeout.test.js +0 -58
- package/src/claude/skills/chrome-devtools/scripts/__tests__/error-handling.test.js +0 -102
|
@@ -79,17 +79,6 @@ function addUserHooks(root) {
|
|
|
79
79
|
fs.writeFileSync(hooksPath(root), `${JSON.stringify(config, null, 2)}\n`);
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
test('a fresh install registers the precompact capture hook', () => {
|
|
83
|
-
inTempProject((root) => {
|
|
84
|
-
assert.ok(managedScripts().PreCompact?.includes('precompact.cjs'));
|
|
85
|
-
const result = install(root);
|
|
86
|
-
assert.equal(result.status, 0, result.stderr);
|
|
87
|
-
const scripts = (readHooks(root).hooks.PreCompact || []).flatMap(group => group.hooks || []).map(handler => hookScript(handler.command));
|
|
88
|
-
assert.ok(scripts.includes('precompact.cjs'));
|
|
89
|
-
assert.ok(fs.existsSync(path.join(root, '.codex/hooks/precompact.cjs')));
|
|
90
|
-
});
|
|
91
|
-
});
|
|
92
|
-
|
|
93
82
|
test('a fresh install registers every CafeKit hook under its own event', () => {
|
|
94
83
|
inTempProject((root) => {
|
|
95
84
|
const result = install(root);
|
|
@@ -103,7 +92,10 @@ test('a fresh install registers every CafeKit hook under its own event', () => {
|
|
|
103
92
|
.filter(Boolean);
|
|
104
93
|
for (const script of scripts) {
|
|
105
94
|
assert.ok(registered.includes(script), `${script} missing from ${eventName}`);
|
|
95
|
+
assert.ok(fs.existsSync(path.join(root, '.codex/hooks', script)), `${script} not installed`);
|
|
106
96
|
}
|
|
97
|
+
const template = JSON.parse(fs.readFileSync(TEMPLATE, 'utf8')).hooks[eventName];
|
|
98
|
+
assert.deepEqual(installed.hooks[eventName].map((group) => group.matcher), template.map((group) => group.matcher));
|
|
107
99
|
}
|
|
108
100
|
});
|
|
109
101
|
});
|
|
@@ -119,6 +111,11 @@ test('a reinstall preserves user-added hooks instead of overwriting them', () =>
|
|
|
119
111
|
const commands = allCommands(readHooks(root));
|
|
120
112
|
assert.ok(commands.includes(USER_STOP_COMMAND), 'user Stop hook was erased by the reinstall');
|
|
121
113
|
assert.ok(commands.includes(USER_PRETOOL_COMMAND), 'user PreToolUse hook was erased by the reinstall');
|
|
114
|
+
const after = readHooks(root).hooks;
|
|
115
|
+
const userGroup = (event, command) => after[event].find((group) => group.hooks.some((handler) => handler.command === command));
|
|
116
|
+
assert.deepEqual(userGroup('Stop', USER_STOP_COMMAND), { matcher: '*', hooks: [{ type: 'command', command: USER_STOP_COMMAND }] });
|
|
117
|
+
assert.deepEqual(userGroup('PreToolUse', USER_PRETOOL_COMMAND), { matcher: 'Bash', hooks: [{ type: 'command', command: USER_PRETOOL_COMMAND }] });
|
|
118
|
+
assert.doesNotMatch(second.stdout + second.stderr, /matcher preserved/);
|
|
122
119
|
});
|
|
123
120
|
});
|
|
124
121
|
|
|
@@ -7,6 +7,7 @@ const os = require('node:os');
|
|
|
7
7
|
const path = require('node:path');
|
|
8
8
|
const { spawnSync, spawn } = require('node:child_process');
|
|
9
9
|
const { test } = require('node:test');
|
|
10
|
+
const { installCodexHookRuntime } = require('./codex-hook-runtime-fixture.cjs');
|
|
10
11
|
|
|
11
12
|
const OLD_INSPECTION_MATCHER = 'Bash|exec_command|apply_patch|Edit|Write|Read|Glob|Grep|mcp__.*|shell';
|
|
12
13
|
const NEW_INSPECTION_MATCHER = 'Bash|exec_command|shell|apply_patch|mcp__.*|collaborationspawn_agent|collaborationwait_agent';
|
|
@@ -44,31 +45,6 @@ test('upgrade preserves a mixed user group and warns', () => matcherFixture(f =>
|
|
|
44
45
|
assert.ok(f.read().hooks.PreToolUse[0].hooks.some(h => h.command === 'echo user-owned'));
|
|
45
46
|
assert.ok(f.warnings.some(line => /matcher.*preserv/i.test(line)));
|
|
46
47
|
}));
|
|
47
|
-
test('reinstall keeps purely user groups unchanged without matcher warnings', () => matcherFixture(f => {
|
|
48
|
-
const userPre = { matcher: 'Bash', hooks: [{ command: 'my-linter' }] };
|
|
49
|
-
const userPost = { hooks: [{ command: 'my-logger' }] };
|
|
50
|
-
f.config.hooks = { PreToolUse: [userPre], PostToolUse: [userPost] }; f.save();
|
|
51
|
-
const installs = Array.from({ length: 2 }, () => {
|
|
52
|
-
const result = spawnSync(process.execPath, [path.resolve(__dirname, '../install.js'), '--platform', 'codex', '-y'], {
|
|
53
|
-
cwd: f.root, encoding: 'utf8',
|
|
54
|
-
});
|
|
55
|
-
return { result, config: f.read() };
|
|
56
|
-
});
|
|
57
|
-
for (const { result, config } of installs) {
|
|
58
|
-
assert.equal(result.status, 0, result.stderr);
|
|
59
|
-
for (const [event, original] of [['PreToolUse', userPre], ['PostToolUse', userPost]]) {
|
|
60
|
-
const group = config.hooks[event].find(g => g.hooks.some(h => h.command === original.hooks[0].command));
|
|
61
|
-
assert.equal(JSON.stringify(group), JSON.stringify(original));
|
|
62
|
-
}
|
|
63
|
-
assert.doesNotMatch(result.stdout + result.stderr, /matcher preserved/);
|
|
64
|
-
}
|
|
65
|
-
}));
|
|
66
|
-
test('fresh hook registration uses every template matcher', () => matcherFixture(f => {
|
|
67
|
-
f.merge();
|
|
68
|
-
for (const [event, groups] of Object.entries(f.config.hooks)) {
|
|
69
|
-
assert.deepEqual(f.read().hooks[event].map(g => g.matcher), groups.map(g => g.matcher));
|
|
70
|
-
}
|
|
71
|
-
}));
|
|
72
48
|
test('upgrade preserves a user-selected managed matcher and warns', () => matcherFixture(f => {
|
|
73
49
|
f.config.hooks.PreToolUse[0].matcher = 'Read|Grep'; f.save(); f.merge();
|
|
74
50
|
assert.equal(f.read().hooks.PreToolUse[0].matcher, 'Read|Grep');
|
|
@@ -93,12 +69,18 @@ const { stateDir: codexStateDir } = require(
|
|
|
93
69
|
path.join(CODEX_HOOKS, 'lib', 'state-store.cjs')
|
|
94
70
|
);
|
|
95
71
|
|
|
72
|
+
// A hook child that never exits must fail its test, not hang the whole suite.
|
|
73
|
+
const HOOK_TIMEOUT_MS = 30000;
|
|
74
|
+
|
|
96
75
|
function runHook(file, cwd, payload) {
|
|
97
|
-
|
|
76
|
+
const result = spawnSync(process.execPath, [file], {
|
|
98
77
|
cwd,
|
|
99
78
|
input: JSON.stringify(payload),
|
|
100
|
-
encoding: 'utf8'
|
|
79
|
+
encoding: 'utf8',
|
|
80
|
+
timeout: HOOK_TIMEOUT_MS
|
|
101
81
|
});
|
|
82
|
+
assert.equal(result.error, undefined, `${path.basename(file)} did not exit: ${result.error?.message}`);
|
|
83
|
+
return result;
|
|
102
84
|
}
|
|
103
85
|
|
|
104
86
|
function runtimeContext(root, feature = 'auth', session = 'session-a') {
|
|
@@ -145,26 +127,7 @@ function inHookFixture(run, options = {}) {
|
|
|
145
127
|
const hooks = path.join(root, '.codex', 'hooks');
|
|
146
128
|
let deferred = false;
|
|
147
129
|
try {
|
|
148
|
-
|
|
149
|
-
fs.copyFileSync(path.join(PACKAGE_ROOT, 'src/claude/hooks/lib/spec-touch.cjs'), path.join(hooks, 'lib/spec-touch.cjs'));
|
|
150
|
-
fs.copyFileSync(path.join(PACKAGE_ROOT, 'src/claude/hooks/lib/secret-keywords.cjs'), path.join(hooks, 'lib/secret-keywords.cjs'));
|
|
151
|
-
fs.copyFileSync(path.join(PACKAGE_ROOT, 'src/claude/hooks/lib/update-check.cjs'), path.join(hooks, 'lib/update-check.cjs'));
|
|
152
|
-
fs.copyFileSync(
|
|
153
|
-
path.join(PACKAGE_ROOT, 'src/claude/hooks/lib/inspect-paths.cjs'),
|
|
154
|
-
path.join(hooks, 'lib/inspect-paths.cjs'),
|
|
155
|
-
);
|
|
156
|
-
fs.copyFileSync(
|
|
157
|
-
path.join(PACKAGE_ROOT, 'src/claude/hooks/lib/privacy-command-analysis.cjs'),
|
|
158
|
-
path.join(hooks, 'lib/privacy-command-analysis.cjs'),
|
|
159
|
-
);
|
|
160
|
-
fs.copyFileSync(
|
|
161
|
-
path.join(PACKAGE_ROOT, 'src/claude/hooks/lib/privacy-paths.cjs'),
|
|
162
|
-
path.join(hooks, 'lib/privacy-paths.cjs'),
|
|
163
|
-
);
|
|
164
|
-
fs.copyFileSync(
|
|
165
|
-
path.join(PACKAGE_ROOT, 'src/claude/hooks/lib/runtime-path-safety.cjs'),
|
|
166
|
-
path.join(hooks, 'lib/runtime-path-safety.cjs'),
|
|
167
|
-
);
|
|
130
|
+
installCodexHookRuntime(root);
|
|
168
131
|
fs.mkdirSync(path.join(root, '.codex', 'scripts'), { recursive: true });
|
|
169
132
|
if (options.policy !== 'missing') {
|
|
170
133
|
if (options.policy === 'malformed') {
|
|
@@ -216,15 +179,7 @@ function inNoCommitCodexFixture(run) {
|
|
|
216
179
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-codex-no-commit-'));
|
|
217
180
|
const hooks = path.join(root, '.codex/hooks');
|
|
218
181
|
try {
|
|
219
|
-
|
|
220
|
-
for (const name of ['privacy-paths.cjs', 'privacy-command-analysis.cjs']) {
|
|
221
|
-
fs.copyFileSync(path.join(PACKAGE_ROOT, 'src/claude/hooks/lib', name), path.join(hooks, 'lib', name));
|
|
222
|
-
}
|
|
223
|
-
fs.copyFileSync(path.join(PACKAGE_ROOT, 'src/claude/hooks/lib/spec-touch.cjs'), path.join(hooks, 'lib/spec-touch.cjs'));
|
|
224
|
-
fs.copyFileSync(path.join(PACKAGE_ROOT, 'src/claude/hooks/lib/secret-keywords.cjs'), path.join(hooks, 'lib/secret-keywords.cjs'));
|
|
225
|
-
fs.copyFileSync(path.join(PACKAGE_ROOT, 'src/claude/hooks/lib/update-check.cjs'), path.join(hooks, 'lib/update-check.cjs'));
|
|
226
|
-
fs.copyFileSync(path.join(PACKAGE_ROOT, 'src/claude/hooks/lib/runtime-path-safety.cjs'),
|
|
227
|
-
path.join(hooks, 'lib/runtime-path-safety.cjs'));
|
|
182
|
+
installCodexHookRuntime(root);
|
|
228
183
|
const scripts = path.join(root, '.codex/scripts');
|
|
229
184
|
fs.mkdirSync(scripts, { recursive: true });
|
|
230
185
|
for (const name of ['workflow-policy.cjs', 'provenance.cjs', 'spec-receipt.cjs', 'spec-resolver.cjs']) {
|
|
@@ -539,7 +494,7 @@ for (const method of ['spawnSync', 'execFileSync']) {
|
|
|
539
494
|
}
|
|
540
495
|
`);
|
|
541
496
|
const result = spawnSync(process.execPath, [path.join(hooks, 'spec-state.cjs')], {
|
|
542
|
-
cwd: root, encoding: 'utf8',
|
|
497
|
+
cwd: root, encoding: 'utf8', timeout: HOOK_TIMEOUT_MS,
|
|
543
498
|
env: { ...process.env, NODE_OPTIONS: `${process.env.NODE_OPTIONS || ''} --require ${JSON.stringify(preload)}` },
|
|
544
499
|
input: JSON.stringify({ cwd: root, featureName: 'auth', hook_event_name: 'UserPromptSubmit',
|
|
545
500
|
session_id: 'one-snapshot', prompt: 'Continue specs/auth' }),
|
|
@@ -754,7 +709,7 @@ test('Codex secret guardrail runs from source with its shared keyword shim', ()
|
|
|
754
709
|
delete env.CLAUDE_PROJECT_DIR;
|
|
755
710
|
delete env.PROJECT_ROOT;
|
|
756
711
|
const result = spawnSync(process.execPath, [path.join(CODEX_HOOKS, 'secret-output-guardrail.cjs')], {
|
|
757
|
-
cwd: PACKAGE_ROOT, env, encoding: 'utf8', input: JSON.stringify({ prompt: 'Explain secret handling.' }),
|
|
712
|
+
cwd: PACKAGE_ROOT, env, encoding: 'utf8', timeout: HOOK_TIMEOUT_MS, input: JSON.stringify({ prompt: 'Explain secret handling.' }),
|
|
758
713
|
});
|
|
759
714
|
assert.equal(result.status, 0);
|
|
760
715
|
assert.equal(result.stderr, '');
|
|
@@ -804,6 +759,37 @@ test('Codex compact clears its pending privacy request even with an invalid spec
|
|
|
804
759
|
});
|
|
805
760
|
});
|
|
806
761
|
|
|
762
|
+
// Two privacy blocks racing on a fresh project both see no `.privacy` directory;
|
|
763
|
+
// the second mkdir then meets the directory the first one made.
|
|
764
|
+
test('Codex privacy records a request when another hook creates its state directory first', () => {
|
|
765
|
+
inHookFixture((root, hooks) => {
|
|
766
|
+
const privacy = require(path.join(hooks, 'lib/privacy-state.cjs'));
|
|
767
|
+
fs.mkdirSync(path.join(hooks, '.privacy'), { mode: 0o700 });
|
|
768
|
+
const existsSync = fs.existsSync;
|
|
769
|
+
fs.existsSync = target => (path.basename(String(target)) === '.privacy' ? false : existsSync(target));
|
|
770
|
+
let request;
|
|
771
|
+
try {
|
|
772
|
+
request = privacy.createPending({ projectRoot: root, sessionCwd: root, sessionId: 'race-session', filePaths: ['.env'] });
|
|
773
|
+
} finally {
|
|
774
|
+
fs.existsSync = existsSync;
|
|
775
|
+
}
|
|
776
|
+
assert.match(request.requestId, /^[a-f0-9]{24}$/);
|
|
777
|
+
});
|
|
778
|
+
});
|
|
779
|
+
|
|
780
|
+
test('Codex privacy denies when its state path is a regular file', () => {
|
|
781
|
+
inHookFixture((root, hooks) => {
|
|
782
|
+
fs.writeFileSync(path.join(hooks, '.privacy'), '');
|
|
783
|
+
const result = runHook(path.join(hooks, 'privacy-block.cjs'), root, {
|
|
784
|
+
cwd: root, session_id: 'file-state', hook_event_name: 'PreToolUse', tool_name: 'Bash', tool_input: { command: 'cat .env' },
|
|
785
|
+
});
|
|
786
|
+
assert.equal(result.status, 0);
|
|
787
|
+
const output = JSON.parse(result.stdout).hookSpecificOutput;
|
|
788
|
+
assert.equal(output.permissionDecision, 'deny');
|
|
789
|
+
assert.match(output.permissionDecisionReason, /could not be evaluated safely/);
|
|
790
|
+
});
|
|
791
|
+
});
|
|
792
|
+
|
|
807
793
|
test('Codex compact releases only its own rules reservation', () => {
|
|
808
794
|
inHookFixture((root, hooks) => {
|
|
809
795
|
fs.writeFileSync(path.join(root, '.codex/runtime.json'), '{}');
|
|
@@ -824,7 +810,7 @@ test('Codex compact releases only its own rules reservation', () => {
|
|
|
824
810
|
test('Codex precompact ignores empty stdin without recording a crash', () => {
|
|
825
811
|
inHookFixture((root, hooks) => {
|
|
826
812
|
const result = spawnSync(process.execPath, [path.join(hooks, 'precompact.cjs')], {
|
|
827
|
-
cwd: root, input: '', encoding: 'utf8',
|
|
813
|
+
cwd: root, input: '', encoding: 'utf8', timeout: HOOK_TIMEOUT_MS,
|
|
828
814
|
});
|
|
829
815
|
assert.equal(result.status, 0);
|
|
830
816
|
assert.equal(result.stderr, '');
|
|
@@ -869,7 +855,8 @@ function sessionWithoutUpdateNetwork(root, hooks, session_id) {
|
|
|
869
855
|
throw new Error('network disabled in update test');
|
|
870
856
|
};\n`);
|
|
871
857
|
const result = spawnSync(process.execPath, ['--require', preload, path.join(hooks, 'session.cjs')], {
|
|
872
|
-
cwd: root, encoding: 'utf8',
|
|
858
|
+
cwd: root, encoding: 'utf8', timeout: HOOK_TIMEOUT_MS,
|
|
859
|
+
input: JSON.stringify({ cwd: root, hook_event_name: 'SessionStart', source: 'startup', session_id }),
|
|
873
860
|
});
|
|
874
861
|
assert.equal(fs.existsSync(marker), false, 'the cached update check must not attempt a network request');
|
|
875
862
|
return result;
|
|
@@ -945,7 +932,7 @@ function shortPrivacyFixture(check) {
|
|
|
945
932
|
hook_event_name: 'PreToolUse', tool_name: 'Bash', tool_input: { command: `cat ${file}` } });
|
|
946
933
|
const block = (file, id) => runHook(path.join(hooks, 'privacy-block.cjs'), root, payload(file, id));
|
|
947
934
|
const submit = (prompt, session_id = session, env = process.env) => spawnSync(process.execPath,
|
|
948
|
-
[path.join(hooks, 'privacy-approval.cjs')], { cwd: root, encoding: 'utf8', env,
|
|
935
|
+
[path.join(hooks, 'privacy-approval.cjs')], { cwd: root, encoding: 'utf8', env, timeout: HOOK_TIMEOUT_MS,
|
|
949
936
|
input: JSON.stringify({ cwd: root, session_id, hook_event_name: 'UserPromptSubmit', prompt }) });
|
|
950
937
|
const files = prefix => fs.existsSync(dir) ? fs.readdirSync(dir).filter(name => name.startsWith(prefix)) : [];
|
|
951
938
|
const id = result => {
|
|
@@ -1651,6 +1638,7 @@ test('empty or forged Codex hook authority fails closed', () => {
|
|
|
1651
1638
|
env: { ...process.env },
|
|
1652
1639
|
input,
|
|
1653
1640
|
encoding: 'utf8',
|
|
1641
|
+
timeout: HOOK_TIMEOUT_MS,
|
|
1654
1642
|
});
|
|
1655
1643
|
assert.equal(result.status, 0, `${JSON.stringify(input)}: ${result.stderr}`);
|
|
1656
1644
|
assert.equal(result.stderr, '');
|
|
@@ -2130,14 +2118,18 @@ test('Codex privacy denies sensitive names it can resolve, allows pure indirecti
|
|
|
2130
2118
|
{ command: 'curl --config ~/.netrc https://example.com', tool: 'Bash' },
|
|
2131
2119
|
{ command: 'kubectl --kubeconfig ~/.kube/kubeconfig get pods', tool: 'exec_command' },
|
|
2132
2120
|
{ command: 'openssl rsa -in server.key', tool: 'Bash' },
|
|
2121
|
+
{ command: 'less "$HOME/.env"', tool: 'Bash' },
|
|
2122
|
+
{ command: 'cat $(echo .env)', tool: 'Bash' },
|
|
2123
|
+
{ command: 'cat `echo .env`', tool: 'Bash' },
|
|
2133
2124
|
];
|
|
2134
2125
|
for (const { command, tool } of denied) {
|
|
2135
2126
|
const res = runHook(block, root, { cwd: root, session_id: 's1', hook_event_name: 'PreToolUse', tool_name: tool, tool_input: { command } });
|
|
2136
2127
|
assert.equal(JSON.parse(res.stdout).hookSpecificOutput.permissionDecision, 'deny', `should deny ${command}`);
|
|
2137
2128
|
}
|
|
2138
|
-
// Accepted gap: the target of `$FILE` is unknowable without running
|
|
2139
|
-
// shell, and every denial here costs a round trip through a one-shot
|
|
2140
|
-
// approval prompt.
|
|
2129
|
+
// Accepted gap: the target of a bare `$FILE` is unknowable without running
|
|
2130
|
+
// the shell, and every denial here costs a round trip through a one-shot
|
|
2131
|
+
// approval prompt. A sensitive name spelled out next to the expansion
|
|
2132
|
+
// (`"$HOME/.env"`, `$(echo .env)`) is not indirection and is denied above.
|
|
2141
2133
|
const allowed = [
|
|
2142
2134
|
{ command: "echo $VAR", tool: 'exec_command' },
|
|
2143
2135
|
{ command: 'echo "$HOME"', tool: 'Bash' },
|
|
@@ -2166,11 +2158,8 @@ test('Codex privacy denies sensitive names it can resolve, allows pure indirecti
|
|
|
2166
2158
|
{ command: 'cat "$FILE"', tool: 'exec_command' },
|
|
2167
2159
|
{ command: 'cat ${FILE}', tool: 'Bash' },
|
|
2168
2160
|
{ command: 'cat $1', tool: 'exec_command' },
|
|
2169
|
-
{ command: 'cat $(echo .env)', tool: 'Bash' },
|
|
2170
|
-
{ command: 'cat `echo .env`', tool: 'Bash' },
|
|
2171
2161
|
{ command: 'head $SECRET', tool: 'exec_command' },
|
|
2172
2162
|
{ command: 'tail ${MY_FILE}', tool: 'Bash' },
|
|
2173
|
-
{ command: 'less "$HOME/.env"', tool: 'Bash' },
|
|
2174
2163
|
{ command: 'cat README.md $EXTRA', tool: 'Bash' },
|
|
2175
2164
|
{ command: 'echo ok; cat README.md $FILE', tool: 'Bash' },
|
|
2176
2165
|
{ command: 'cat README.md | head $FILE', tool: 'exec_command' },
|
|
@@ -2358,7 +2347,37 @@ test('Codex hook-context canonicalizes symlink project root (regression)', () =>
|
|
|
2358
2347
|
});
|
|
2359
2348
|
});
|
|
2360
2349
|
|
|
2361
|
-
test('Codex
|
|
2350
|
+
test('Codex session state refuses a symlinked state path and writes nothing outside the project', () => {
|
|
2351
|
+
inHookFixture((root, hooks) => {
|
|
2352
|
+
const stateStore = require(path.join(hooks, 'lib', 'state-store.cjs'));
|
|
2353
|
+
const sessionId = 'sess-symlink';
|
|
2354
|
+
const dir = stateStore.stateDir(root, sessionId);
|
|
2355
|
+
const sessionState = path.dirname(dir);
|
|
2356
|
+
// A repository can commit either the shared state folder or one session folder as a link.
|
|
2357
|
+
for (const link of [sessionState, dir]) {
|
|
2358
|
+
const outside = fs.mkdtempSync(path.join(os.tmpdir(), 'codex-state-outside-'));
|
|
2359
|
+
try {
|
|
2360
|
+
fs.rmSync(sessionState, { recursive: true, force: true });
|
|
2361
|
+
fs.mkdirSync(path.dirname(link), { recursive: true });
|
|
2362
|
+
fs.symlinkSync(outside, link);
|
|
2363
|
+
let called = false;
|
|
2364
|
+
const ok = stateStore.withStateLock(root, sessionId, (stateDir) => {
|
|
2365
|
+
called = true;
|
|
2366
|
+
stateStore.writeData(stateDir, { todos: [] });
|
|
2367
|
+
});
|
|
2368
|
+
assert.equal(ok, false, `${path.relative(root, link)} link must be refused`);
|
|
2369
|
+
assert.equal(called, false);
|
|
2370
|
+
assert.deepEqual(fs.readdirSync(outside), [], 'nothing may be written outside the project');
|
|
2371
|
+
} finally {
|
|
2372
|
+
fs.rmSync(sessionState, { recursive: true, force: true });
|
|
2373
|
+
fs.rmSync(outside, { recursive: true, force: true });
|
|
2374
|
+
}
|
|
2375
|
+
}
|
|
2376
|
+
assert.equal(stateStore.withStateLock(root, sessionId, () => {}), true, 'a plain state folder still locks');
|
|
2377
|
+
});
|
|
2378
|
+
});
|
|
2379
|
+
|
|
2380
|
+
test('Codex resolver rejects symlink escape', () => {
|
|
2362
2381
|
inHookFixture((root, hooks) => {
|
|
2363
2382
|
const specUtils = require(path.join(hooks, 'lib', 'spec-utils.cjs'));
|
|
2364
2383
|
const resolverPath = path.join(root, '.codex', 'scripts', 'spec-resolver.cjs');
|
|
@@ -2386,23 +2405,6 @@ test('Codex resolver and state reject symlink escape', () => {
|
|
|
2386
2405
|
);
|
|
2387
2406
|
fs.rmSync(outsideScripts, { recursive: true, force: true });
|
|
2388
2407
|
}
|
|
2389
|
-
// State dir symlink escape: create symlink for session-state dir
|
|
2390
|
-
const stateStore = require(path.join(hooks, 'lib', 'state-store.cjs'));
|
|
2391
|
-
const sessionId = 'sess-symlink';
|
|
2392
|
-
const dir = stateStore.stateDir(root, sessionId);
|
|
2393
|
-
fs.mkdirSync(path.dirname(dir), { recursive: true });
|
|
2394
|
-
// Create a symlink that points outside project
|
|
2395
|
-
const outside = fs.mkdtempSync(path.join(os.tmpdir(), 'outside-'));
|
|
2396
|
-
try {
|
|
2397
|
-
if (fs.existsSync(dir)) fs.rmSync(dir, { recursive: true, force: true });
|
|
2398
|
-
fs.symlinkSync(outside, dir);
|
|
2399
|
-
// withStateLock should handle symlink safely (no crash, no write outside)
|
|
2400
|
-
const ok = stateStore.withStateLock(root, sessionId, () => {});
|
|
2401
|
-
assert.equal(typeof ok, 'boolean');
|
|
2402
|
-
} finally {
|
|
2403
|
-
try { fs.unlinkSync(dir); } catch {}
|
|
2404
|
-
fs.rmSync(outside, { recursive: true, force: true });
|
|
2405
|
-
}
|
|
2406
2408
|
});
|
|
2407
2409
|
});
|
|
2408
2410
|
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
// Codex ignores the `disable-model-invocation` key that keeps a skill explicit-only on Claude Code. Its own
|
|
2
|
+
// mechanism is `agents/openai.yaml` beside SKILL.md with `policy.allow_implicit_invocation: false`. These cases
|
|
3
|
+
// pin that the Codex install writes that file for every explicit-only skill through the ownership-aware writer
|
|
4
|
+
// (so upgrades and removals stay clean), writes it for no other skill, and that the catalog never lists such a skill.
|
|
5
|
+
const assert = require('node:assert/strict');
|
|
6
|
+
const fs = require('node:fs');
|
|
7
|
+
const os = require('node:os');
|
|
8
|
+
const path = require('node:path');
|
|
9
|
+
const test = require('node:test');
|
|
10
|
+
const { spawnSync } = require('node:child_process');
|
|
11
|
+
|
|
12
|
+
const { PLATFORMS } = require('../lib/context');
|
|
13
|
+
const manifestLib = require('../lib/manifest');
|
|
14
|
+
const { removeSkill } = require('../phases/skill-inventory');
|
|
15
|
+
|
|
16
|
+
const PACKAGE_ROOT = path.join(__dirname, '..', '..');
|
|
17
|
+
const INSTALLER = path.join(PACKAGE_ROOT, 'bin/install.js');
|
|
18
|
+
const SOURCE_SKILLS = path.join(PACKAGE_ROOT, 'src/claude/skills');
|
|
19
|
+
const POLICY = 'policy:\n allow_implicit_invocation: false\n';
|
|
20
|
+
|
|
21
|
+
const explicitOnly = fs.readdirSync(SOURCE_SKILLS)
|
|
22
|
+
.filter((name) => fs.existsSync(path.join(SOURCE_SKILLS, name, 'SKILL.md')))
|
|
23
|
+
.filter((name) => /^disable-model-invocation:\s*(["']?)true\1\s*$/m.test(
|
|
24
|
+
fs.readFileSync(path.join(SOURCE_SKILLS, name, 'SKILL.md'), 'utf8').split(/^---\s*$/m)[1] || ''));
|
|
25
|
+
|
|
26
|
+
function install(root, platform) {
|
|
27
|
+
const result = spawnSync(process.execPath, [INSTALLER, '--platform', platform, '--yes'],
|
|
28
|
+
{ cwd: root, encoding: 'utf8', env: { ...process.env, PATH: '/usr/bin:/bin' } });
|
|
29
|
+
assert.equal(result.status, 0, result.stderr || result.stdout);
|
|
30
|
+
return result;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function withTempProject(run) {
|
|
34
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-codex-policy-'));
|
|
35
|
+
const previous = process.cwd();
|
|
36
|
+
try {
|
|
37
|
+
assert.equal(spawnSync('git', ['-C', root, 'init', '-q']).status, 0);
|
|
38
|
+
process.chdir(root);
|
|
39
|
+
return run(root);
|
|
40
|
+
} finally {
|
|
41
|
+
process.chdir(previous);
|
|
42
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const policyFile = (root, skill) => path.join(root, '.agents/skills', skill, 'agents/openai.yaml');
|
|
47
|
+
const ownershipKey = (root, file) => path.relative(path.join(root, PLATFORMS.codex.ownership?.recordRoot || PLATFORMS.codex.folder), file).replace(/\\/g, '/');
|
|
48
|
+
const readOwnership = (root) => JSON.parse(fs.readFileSync(path.join(root, PLATFORMS.codex.folder, manifestLib.MANIFEST_FILE), 'utf8'));
|
|
49
|
+
const writeOwnership = (root, data) => fs.writeFileSync(path.join(root, PLATFORMS.codex.folder, manifestLib.MANIFEST_FILE), JSON.stringify(data, null, 2));
|
|
50
|
+
|
|
51
|
+
function catalog(root, script, skillsRoot) {
|
|
52
|
+
const result = spawnSync(process.execPath, [path.join(root, script), '--json', '--root', path.join(root, skillsRoot)],
|
|
53
|
+
{ cwd: root, encoding: 'utf8' });
|
|
54
|
+
assert.equal(result.status, 0, result.stderr);
|
|
55
|
+
return JSON.parse(result.stdout);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
test('the explicit-only skills are adv, toaa and vibe', () => {
|
|
59
|
+
assert.deepEqual(explicitOnly.sort(), ['adv', 'toaa', 'vibe']);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('a Codex install writes the policy file for every explicit-only skill, owned, and for no other skill', () => {
|
|
63
|
+
withTempProject((root) => {
|
|
64
|
+
install(root, 'codex');
|
|
65
|
+
const ownership = readOwnership(root);
|
|
66
|
+
for (const skill of explicitOnly) {
|
|
67
|
+
const file = policyFile(root, skill);
|
|
68
|
+
assert.equal(fs.readFileSync(file, 'utf8'), POLICY, skill);
|
|
69
|
+
assert.equal(ownership.files[ownershipKey(root, file)]?.sha256, manifestLib.hashFile(file), `${skill} policy is not owned`);
|
|
70
|
+
}
|
|
71
|
+
const others = fs.readdirSync(path.join(root, '.agents/skills')).filter((name) => !explicitOnly.includes(name));
|
|
72
|
+
assert.ok(others.length > 10);
|
|
73
|
+
for (const skill of others) assert.equal(fs.existsSync(policyFile(root, skill)), false, skill);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('reinstalling keeps the policy bytes and its ownership record unchanged', () => {
|
|
78
|
+
withTempProject((root) => {
|
|
79
|
+
install(root, 'codex');
|
|
80
|
+
const before = explicitOnly.map((skill) => readOwnership(root).files[ownershipKey(root, policyFile(root, skill))]?.sha256);
|
|
81
|
+
install(root, 'codex');
|
|
82
|
+
const after = explicitOnly.map((skill) => readOwnership(root).files[ownershipKey(root, policyFile(root, skill))]?.sha256);
|
|
83
|
+
assert.deepEqual(after, before);
|
|
84
|
+
for (const skill of explicitOnly) assert.equal(fs.readFileSync(policyFile(root, skill), 'utf8'), POLICY);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test('an owned policy file under a skill that dropped the key is removed on upgrade; a user-created one stays', () => {
|
|
89
|
+
withTempProject((root) => {
|
|
90
|
+
install(root, 'codex');
|
|
91
|
+
// An earlier release that marked ask explicit-only would have written and owned this file.
|
|
92
|
+
const owned = policyFile(root, 'ask');
|
|
93
|
+
fs.mkdirSync(path.dirname(owned), { recursive: true });
|
|
94
|
+
fs.writeFileSync(owned, POLICY);
|
|
95
|
+
const data = readOwnership(root);
|
|
96
|
+
data.files[ownershipKey(root, owned)] = { sha256: manifestLib.hashFile(owned), version: 'old' };
|
|
97
|
+
writeOwnership(root, data);
|
|
98
|
+
// A file the user wrote is never ours to delete.
|
|
99
|
+
const userFile = policyFile(root, 'debug');
|
|
100
|
+
fs.mkdirSync(path.dirname(userFile), { recursive: true });
|
|
101
|
+
fs.writeFileSync(userFile, POLICY);
|
|
102
|
+
|
|
103
|
+
install(root, 'codex');
|
|
104
|
+
assert.equal(fs.existsSync(owned), false, 'owned stale policy survived the upgrade');
|
|
105
|
+
assert.equal(readOwnership(root).files[ownershipKey(root, owned)], undefined, 'stale policy stayed in the manifest');
|
|
106
|
+
assert.equal(fs.readFileSync(userFile, 'utf8'), POLICY, 'user-created policy was removed');
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test('a user edit to the policy file is preserved on upgrade', () => {
|
|
111
|
+
withTempProject((root) => {
|
|
112
|
+
install(root, 'codex');
|
|
113
|
+
const file = policyFile(root, 'vibe');
|
|
114
|
+
const edited = 'policy:\n allow_implicit_invocation: true\n';
|
|
115
|
+
fs.writeFileSync(file, edited);
|
|
116
|
+
install(root, 'codex');
|
|
117
|
+
assert.equal(fs.readFileSync(file, 'utf8'), edited);
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test('removing an explicit-only skill removes its whole tree, the policy file included', () => {
|
|
122
|
+
withTempProject((root) => {
|
|
123
|
+
install(root, 'codex');
|
|
124
|
+
const ctx = {
|
|
125
|
+
ownership: { [PLATFORMS.codex.folder]: readOwnership(root) },
|
|
126
|
+
trackers: { codex: manifestLib.createTracker(PLATFORMS.codex.folder, 'next', PLATFORMS.codex.ownership) },
|
|
127
|
+
dryRun: false,
|
|
128
|
+
ui: { warn() {}, detail() {} },
|
|
129
|
+
results: { updated: 0, preserved: 0, preservedFiles: [] }
|
|
130
|
+
};
|
|
131
|
+
removeSkill(ctx, 'codex', 'vibe');
|
|
132
|
+
assert.deepEqual(ctx.results.preservedFiles, []);
|
|
133
|
+
assert.equal(fs.existsSync(path.join(root, '.agents/skills/vibe')), false);
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test('a Claude install writes no policy file', () => {
|
|
138
|
+
withTempProject((root) => {
|
|
139
|
+
install(root, 'claude');
|
|
140
|
+
for (const skill of fs.readdirSync(path.join(root, '.claude/skills'))) {
|
|
141
|
+
assert.equal(fs.existsSync(path.join(root, '.claude/skills', skill, 'agents/openai.yaml')), false, skill);
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test('neither runtime catalog lists an explicit-only skill, and both still list the others', () => {
|
|
147
|
+
withTempProject((root) => {
|
|
148
|
+
install(root, 'claude,codex');
|
|
149
|
+
for (const [script, skillsRoot] of [['.codex/scripts/generate-skill-catalog.cjs', '.agents/skills'],
|
|
150
|
+
['.claude/scripts/generate-skill-catalog.cjs', '.claude/skills']]) {
|
|
151
|
+
const parsed = catalog(root, script, skillsRoot);
|
|
152
|
+
const ids = parsed.skills.map((skill) => skill.public_id);
|
|
153
|
+
for (const skill of explicitOnly) {
|
|
154
|
+
assert.equal(ids.includes(`cf:${skill}`), false, `${script} lists cf:${skill}`);
|
|
155
|
+
assert.ok(parsed.diagnostics.some((item) => item.directory === skill && item.code === 'explicit_only'), `${script}: ${skill}`);
|
|
156
|
+
}
|
|
157
|
+
assert.ok(ids.includes('cf:ask') && ids.includes('cf:specs'), script);
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
});
|
|
@@ -6,16 +6,14 @@ const os = require('node:os');
|
|
|
6
6
|
const path = require('node:path');
|
|
7
7
|
const { spawnSync } = require('node:child_process');
|
|
8
8
|
const { test } = require('node:test');
|
|
9
|
+
const { installCodexHookRuntime } = require('./codex-hook-runtime-fixture.cjs');
|
|
9
10
|
const SOURCE = path.resolve(__dirname, '../../src');
|
|
10
11
|
|
|
11
12
|
function fixture(run) {
|
|
12
13
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'ck-native-boundaries-'));
|
|
13
14
|
const hooks = path.join(root, '.codex/hooks');
|
|
14
15
|
try {
|
|
15
|
-
|
|
16
|
-
for (const name of ['runtime-path-safety', 'privacy-command-analysis', 'privacy-paths', 'secret-keywords', 'inspect-paths', 'update-check']) {
|
|
17
|
-
fs.copyFileSync(path.join(SOURCE, `claude/hooks/lib/${name}.cjs`), path.join(hooks, `lib/${name}.cjs`));
|
|
18
|
-
}
|
|
16
|
+
installCodexHookRuntime(root);
|
|
19
17
|
fs.cpSync(path.join(SOURCE, 'claude/scripts'), path.join(root, '.codex/scripts'), { recursive: true });
|
|
20
18
|
fs.writeFileSync(path.join(root, '.gitignore'), '.codex/\n');
|
|
21
19
|
for (const args of [['init', '-q'], ['config', 'user.email', 'probe@example.invalid'], ['config', 'user.name', 'Probe'], ['add', '.gitignore'], ['commit', '-qm', 'base']]) {
|