javi-forge 1.31.0 → 1.33.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/dist/cli/dispatch/hooks.d.ts +5 -2
- package/dist/cli/dispatch/hooks.js +16 -2
- package/dist/cli/help.d.ts +1 -1
- package/dist/cli/help.js +12 -2
- package/dist/commands/claude-hooks.d.ts +36 -0
- package/dist/commands/claude-hooks.js +98 -0
- package/dist/commands/init/steps/security.d.ts +7 -3
- package/dist/commands/init/steps/security.js +25 -19
- package/dist/lib/claude-hook-manager.d.ts +87 -0
- package/dist/lib/claude-hook-manager.js +196 -2
- package/dist/lib/claude-hook-settings.d.ts +18 -0
- package/dist/lib/claude-hook-settings.js +16 -0
- package/dist/types/index.d.ts +5 -0
- package/dist/ui/App.js +3 -0
- package/package.json +1 -1
|
@@ -4,8 +4,11 @@
|
|
|
4
4
|
* (`./commands/hooks.js`) is lazy-imported inside the handler to keep cold-start
|
|
5
5
|
* minimal, because hooks are on the commit/push hot path.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
7
|
+
* Subcommands:
|
|
8
|
+
* - `hooks run <pre-commit|pre-push>` → runHook, exits with its code.
|
|
9
|
+
* - `hooks <install|doctor|repair> claude [--force]` → runClaudeHookCommand,
|
|
10
|
+
* exits with its code (wrong/missing target → usage + exit 1).
|
|
11
|
+
* Any other subcommand or a missing name → usage + exit 1.
|
|
9
12
|
*/
|
|
10
13
|
import type { CLI } from "./types.js";
|
|
11
14
|
export declare function handleHooks(cli: CLI): Promise<void>;
|
|
@@ -4,8 +4,11 @@
|
|
|
4
4
|
* (`./commands/hooks.js`) is lazy-imported inside the handler to keep cold-start
|
|
5
5
|
* minimal, because hooks are on the commit/push hot path.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
7
|
+
* Subcommands:
|
|
8
|
+
* - `hooks run <pre-commit|pre-push>` → runHook, exits with its code.
|
|
9
|
+
* - `hooks <install|doctor|repair> claude [--force]` → runClaudeHookCommand,
|
|
10
|
+
* exits with its code (wrong/missing target → usage + exit 1).
|
|
11
|
+
* Any other subcommand or a missing name → usage + exit 1.
|
|
9
12
|
*/
|
|
10
13
|
import { HOOKS_HELP_TEXT } from "../help.js";
|
|
11
14
|
export async function handleHooks(cli) {
|
|
@@ -23,6 +26,17 @@ export async function handleHooks(cli) {
|
|
|
23
26
|
const code = await runHook(name, process.cwd());
|
|
24
27
|
process.exit(code);
|
|
25
28
|
}
|
|
29
|
+
const sub = cli.input[1];
|
|
30
|
+
if (sub === "install" || sub === "doctor" || sub === "repair") {
|
|
31
|
+
if (cli.input[2] !== "claude") {
|
|
32
|
+
console.error(`Usage: javi-forge hooks ${sub} claude`);
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
const { runClaudeHookCommand } = await import("../../commands/claude-hooks.js");
|
|
36
|
+
process.exit(await runClaudeHookCommand(sub, process.cwd(), {
|
|
37
|
+
force: cli.flags.force === true,
|
|
38
|
+
}));
|
|
39
|
+
}
|
|
26
40
|
// No subcommand → show usage (exit 0). An unknown subcommand is a typo →
|
|
27
41
|
// show usage but exit 1 rather than run nothing silently.
|
|
28
42
|
console.log(HOOKS_HELP_TEXT);
|
package/dist/cli/help.d.ts
CHANGED
|
@@ -19,7 +19,7 @@ export declare const CI_HELP_TEXT = "\n Usage\n $ javi-forge ci [subcommand]
|
|
|
19
19
|
* Per-command help for `hooks`, shown by `javi-forge hooks --help` (or when
|
|
20
20
|
* `hooks` is given an unknown subcommand). Whitespace is significant.
|
|
21
21
|
*/
|
|
22
|
-
export declare const HOOKS_HELP_TEXT = "\n Usage\n $ javi-forge hooks run <pre-commit|pre-push>\n\n Run the sections enabled under hooks: in .javi-forge/ci.yaml, in a fixed\n cheap\u2192expensive order, fail-fast. With no hooks: config the default is the\n quick native CI gate (setup + lint + compile + gates \u2014 no tests, no coverage).\n\n Subcommands\n run pre-commit
|
|
22
|
+
export declare const HOOKS_HELP_TEXT = "\n Usage\n $ javi-forge hooks run <pre-commit|pre-push>\n $ javi-forge hooks <install|doctor|repair> claude [--force]\n\n Run the sections enabled under hooks: in .javi-forge/ci.yaml, in a fixed\n cheap\u2192expensive order, fail-fast. With no hooks: config the default is the\n quick native CI gate (setup + lint + compile + gates \u2014 no tests, no coverage).\n\n Subcommands\n run pre-commit Run the composed pre-commit sections\n run pre-push Run the composed pre-push sections\n install claude Install the managed Claude PreToolUse guard (.claude/)\n doctor claude Report Claude PreToolUse guard health (informational)\n repair claude Repair the managed guard; --force overwrites edited assets\n\n Notes\n A blocking section failure exits non-zero and blocks the commit/push.\n A broken .javi-forge/ci.yaml exits 1 (fail-closed \u2014 never skips a gate).\n To skip: git commit --no-verify (pre-push: git push --no-verify)\n doctor claude is informational (always exits 0); install/repair exit 0 on\n success, non-zero on refusal/failure. Use repair claude --force to overwrite\n a locally edited managed asset.\n\n Examples\n $ javi-forge hooks run pre-commit\n $ javi-forge hooks run pre-push\n $ javi-forge hooks install claude\n $ javi-forge hooks doctor claude\n $ javi-forge hooks repair claude --force\n";
|
|
23
23
|
export declare const FLAGS_SCHEMA: {
|
|
24
24
|
readonly help: {
|
|
25
25
|
readonly type: "boolean";
|
package/dist/cli/help.js
CHANGED
|
@@ -166,23 +166,33 @@ export const CI_HELP_TEXT = `
|
|
|
166
166
|
export const HOOKS_HELP_TEXT = `
|
|
167
167
|
Usage
|
|
168
168
|
$ javi-forge hooks run <pre-commit|pre-push>
|
|
169
|
+
$ javi-forge hooks <install|doctor|repair> claude [--force]
|
|
169
170
|
|
|
170
171
|
Run the sections enabled under hooks: in .javi-forge/ci.yaml, in a fixed
|
|
171
172
|
cheap→expensive order, fail-fast. With no hooks: config the default is the
|
|
172
173
|
quick native CI gate (setup + lint + compile + gates — no tests, no coverage).
|
|
173
174
|
|
|
174
175
|
Subcommands
|
|
175
|
-
run pre-commit
|
|
176
|
-
run pre-push
|
|
176
|
+
run pre-commit Run the composed pre-commit sections
|
|
177
|
+
run pre-push Run the composed pre-push sections
|
|
178
|
+
install claude Install the managed Claude PreToolUse guard (.claude/)
|
|
179
|
+
doctor claude Report Claude PreToolUse guard health (informational)
|
|
180
|
+
repair claude Repair the managed guard; --force overwrites edited assets
|
|
177
181
|
|
|
178
182
|
Notes
|
|
179
183
|
A blocking section failure exits non-zero and blocks the commit/push.
|
|
180
184
|
A broken .javi-forge/ci.yaml exits 1 (fail-closed — never skips a gate).
|
|
181
185
|
To skip: git commit --no-verify (pre-push: git push --no-verify)
|
|
186
|
+
doctor claude is informational (always exits 0); install/repair exit 0 on
|
|
187
|
+
success, non-zero on refusal/failure. Use repair claude --force to overwrite
|
|
188
|
+
a locally edited managed asset.
|
|
182
189
|
|
|
183
190
|
Examples
|
|
184
191
|
$ javi-forge hooks run pre-commit
|
|
185
192
|
$ javi-forge hooks run pre-push
|
|
193
|
+
$ javi-forge hooks install claude
|
|
194
|
+
$ javi-forge hooks doctor claude
|
|
195
|
+
$ javi-forge hooks repair claude --force
|
|
186
196
|
`;
|
|
187
197
|
export const FLAGS_SCHEMA = {
|
|
188
198
|
// `--help` is handled manually (autoHelp is disabled at the entrypoint so
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `javi-forge hooks <install|doctor|repair> claude` — console-only renderer for
|
|
3
|
+
* the already-tested Claude PreToolUse guard library (Slice 4a). It wires the
|
|
4
|
+
* three lib fns to human output + exit codes; it adds NO new security logic and
|
|
5
|
+
* never touches `runTransaction`/secure-fs directly.
|
|
6
|
+
*
|
|
7
|
+
* Honest-execution gate (Slice 4b): the doctor renderer prints the real
|
|
8
|
+
* `execution` verdict computed by the library — `runnable`, `blocked`, or
|
|
9
|
+
* `inconclusive` — plus its `blockers`, `unknownSources`, and the constant
|
|
10
|
+
* `residual` caveats, in committed stable order. The doctor exit code follows
|
|
11
|
+
* `execution.status` (runnable → 0, blocked → 1, inconclusive → 2), independent
|
|
12
|
+
* of `report.healthy`. The renderer never fabricates `runnable`: the library
|
|
13
|
+
* only reports it when every relevant local source read clear and the guard is
|
|
14
|
+
* current.
|
|
15
|
+
*/
|
|
16
|
+
import { doctorClaudePreToolUse, installClaudePreToolUse, repairClaudePreToolUse } from "../lib/claude-hook-manager.js";
|
|
17
|
+
export type ClaudeHookSub = "install" | "doctor" | "repair";
|
|
18
|
+
/** Injectable seams for tests; each defaults to the real implementation. */
|
|
19
|
+
export interface ClaudeHookCmdDeps {
|
|
20
|
+
install?: typeof installClaudePreToolUse;
|
|
21
|
+
doctor?: typeof doctorClaudePreToolUse;
|
|
22
|
+
repair?: typeof repairClaudePreToolUse;
|
|
23
|
+
log?: (msg: string) => void;
|
|
24
|
+
logError?: (msg: string) => void;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Run one Claude-hook subcommand against `projectDir`. Returns the process exit
|
|
28
|
+
* code (the dispatcher calls `process.exit`, not this fn):
|
|
29
|
+
* - install/repair: 0 when `ok`, 1 on refusal/failure.
|
|
30
|
+
* - doctor: follows `execution.status` — runnable → 0, blocked → 1,
|
|
31
|
+
* inconclusive → 2.
|
|
32
|
+
*/
|
|
33
|
+
export declare function runClaudeHookCommand(sub: ClaudeHookSub, projectDir: string, opts: {
|
|
34
|
+
force?: boolean;
|
|
35
|
+
}, deps?: ClaudeHookCmdDeps): Promise<number>;
|
|
36
|
+
//# sourceMappingURL=claude-hooks.d.ts.map
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `javi-forge hooks <install|doctor|repair> claude` — console-only renderer for
|
|
3
|
+
* the already-tested Claude PreToolUse guard library (Slice 4a). It wires the
|
|
4
|
+
* three lib fns to human output + exit codes; it adds NO new security logic and
|
|
5
|
+
* never touches `runTransaction`/secure-fs directly.
|
|
6
|
+
*
|
|
7
|
+
* Honest-execution gate (Slice 4b): the doctor renderer prints the real
|
|
8
|
+
* `execution` verdict computed by the library — `runnable`, `blocked`, or
|
|
9
|
+
* `inconclusive` — plus its `blockers`, `unknownSources`, and the constant
|
|
10
|
+
* `residual` caveats, in committed stable order. The doctor exit code follows
|
|
11
|
+
* `execution.status` (runnable → 0, blocked → 1, inconclusive → 2), independent
|
|
12
|
+
* of `report.healthy`. The renderer never fabricates `runnable`: the library
|
|
13
|
+
* only reports it when every relevant local source read clear and the guard is
|
|
14
|
+
* current.
|
|
15
|
+
*/
|
|
16
|
+
import { doctorClaudePreToolUse, installClaudePreToolUse, repairClaudePreToolUse, } from "../lib/claude-hook-manager.js";
|
|
17
|
+
function renderMutation(verb, result, log, logError) {
|
|
18
|
+
if (result.ok) {
|
|
19
|
+
log(`${verb} claude: ok`);
|
|
20
|
+
if (result.changed.length > 0) {
|
|
21
|
+
log("changed:");
|
|
22
|
+
for (const p of result.changed)
|
|
23
|
+
log(` ${p}`);
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
log("changed: nothing (already up to date)");
|
|
27
|
+
}
|
|
28
|
+
if (result.backups.length > 0) {
|
|
29
|
+
log("backups:");
|
|
30
|
+
for (const p of result.backups)
|
|
31
|
+
log(` ${p}`);
|
|
32
|
+
}
|
|
33
|
+
return 0;
|
|
34
|
+
}
|
|
35
|
+
logError(`${verb} claude: refused`);
|
|
36
|
+
for (const e of result.errors)
|
|
37
|
+
logError(` ${e}`);
|
|
38
|
+
return 1;
|
|
39
|
+
}
|
|
40
|
+
function renderDoctor(report, log) {
|
|
41
|
+
log(`doctor claude: ${report.healthy ? "healthy" : "unhealthy"}`);
|
|
42
|
+
log(` settings: ${report.settings.state} — ${report.settings.detail}`);
|
|
43
|
+
log(` asset: ${report.asset.state} — ${report.asset.detail}`);
|
|
44
|
+
log(` node: ${report.node.version ?? "unavailable"} (min-satisfied: ${report.node.satisfiesMinimum})`);
|
|
45
|
+
const execution = report.execution;
|
|
46
|
+
log(` execution: ${execution.status}`);
|
|
47
|
+
if (execution.blockers.length > 0) {
|
|
48
|
+
log(" blockers:");
|
|
49
|
+
for (const b of execution.blockers)
|
|
50
|
+
log(` - ${b}`);
|
|
51
|
+
}
|
|
52
|
+
if (execution.unknownSources.length > 0) {
|
|
53
|
+
log(" unknown-sources:");
|
|
54
|
+
for (const u of execution.unknownSources)
|
|
55
|
+
log(` - ${u}`);
|
|
56
|
+
}
|
|
57
|
+
if (execution.residual.length > 0) {
|
|
58
|
+
log(" execution-residual:");
|
|
59
|
+
for (const r of execution.residual)
|
|
60
|
+
log(` - ${r}`);
|
|
61
|
+
}
|
|
62
|
+
log(` host-residual: ${report.hostResidual}`);
|
|
63
|
+
if (report.remediation.length > 0) {
|
|
64
|
+
log(" remediation:");
|
|
65
|
+
for (const r of report.remediation)
|
|
66
|
+
log(` - ${r}`);
|
|
67
|
+
}
|
|
68
|
+
// The doctor exit code follows the effective-execution verdict, independent
|
|
69
|
+
// of component health: runnable → 0, blocked → 1, inconclusive → 2.
|
|
70
|
+
if (execution.status === "blocked")
|
|
71
|
+
return 1;
|
|
72
|
+
if (execution.status === "inconclusive")
|
|
73
|
+
return 2;
|
|
74
|
+
return 0;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Run one Claude-hook subcommand against `projectDir`. Returns the process exit
|
|
78
|
+
* code (the dispatcher calls `process.exit`, not this fn):
|
|
79
|
+
* - install/repair: 0 when `ok`, 1 on refusal/failure.
|
|
80
|
+
* - doctor: follows `execution.status` — runnable → 0, blocked → 1,
|
|
81
|
+
* inconclusive → 2.
|
|
82
|
+
*/
|
|
83
|
+
export async function runClaudeHookCommand(sub, projectDir, opts, deps = {}) {
|
|
84
|
+
const log = deps.log ?? ((m) => console.log(m));
|
|
85
|
+
const logError = deps.logError ?? ((m) => console.error(m));
|
|
86
|
+
const install = deps.install ?? installClaudePreToolUse;
|
|
87
|
+
const doctor = deps.doctor ?? doctorClaudePreToolUse;
|
|
88
|
+
const repair = deps.repair ?? repairClaudePreToolUse;
|
|
89
|
+
if (sub === "install") {
|
|
90
|
+
return renderMutation("install", await install(projectDir), log, logError);
|
|
91
|
+
}
|
|
92
|
+
if (sub === "repair") {
|
|
93
|
+
const result = await repair(projectDir, { force: opts.force === true });
|
|
94
|
+
return renderMutation("repair", result, log, logError);
|
|
95
|
+
}
|
|
96
|
+
return renderDoctor(await doctor(projectDir), log);
|
|
97
|
+
}
|
|
98
|
+
//# sourceMappingURL=claude-hooks.js.map
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import type { StepFn } from "../types.js";
|
|
2
2
|
/**
|
|
3
|
-
* Step 14: Scaffold security hooks (hook-consolidation S4 fold).
|
|
3
|
+
* Step 14: Scaffold security hooks (hook-consolidation S4 fold + SkillGuard 4a).
|
|
4
4
|
*
|
|
5
5
|
* - When options.securityHooks is false, reports "skipped".
|
|
6
6
|
* - Otherwise:
|
|
7
|
-
* 1.
|
|
8
|
-
*
|
|
7
|
+
* 1. When `claudePreToolUseGuard` is set, installs the managed Claude
|
|
8
|
+
* PreToolUse guard via the transactional `installClaudePreToolUse`
|
|
9
|
+
* (SkillGuard Slice 4a). The legacy copy-if-absent
|
|
10
|
+
* `claude-settings-security.json` scaffold is RETIRED — the managed
|
|
11
|
+
* installer owns `.claude/settings.json` + the hook asset with proper
|
|
12
|
+
* ownership markers.
|
|
9
13
|
* 2. Merges the `hooks:` security sections for the selected reliability
|
|
10
14
|
* profile into `.javi-forge/ci.yaml` via `setHookFeature` (creating a
|
|
11
15
|
* minimal `version: 2` config when absent). The dispatcher composes these
|
|
@@ -1,8 +1,5 @@
|
|
|
1
|
-
import path from "node:path";
|
|
2
|
-
import fs from "fs-extra";
|
|
3
|
-
import { SECURITY_HOOKS_DIR } from "../../../constants.js";
|
|
4
1
|
import { setHookFeature } from "../../../lib/ci-config.js";
|
|
5
|
-
import {
|
|
2
|
+
import { installClaudePreToolUse } from "../../../lib/claude-hook-manager.js";
|
|
6
3
|
import { report } from "../report.js";
|
|
7
4
|
/**
|
|
8
5
|
* Hook-feature preset per reliability profile (hook-consolidation S4).
|
|
@@ -21,12 +18,16 @@ const PROFILE_PRESET = {
|
|
|
21
18
|
strict: { preCommit: ["secrets", "permissions"], prePush: ["deps"] },
|
|
22
19
|
};
|
|
23
20
|
/**
|
|
24
|
-
* Step 14: Scaffold security hooks (hook-consolidation S4 fold).
|
|
21
|
+
* Step 14: Scaffold security hooks (hook-consolidation S4 fold + SkillGuard 4a).
|
|
25
22
|
*
|
|
26
23
|
* - When options.securityHooks is false, reports "skipped".
|
|
27
24
|
* - Otherwise:
|
|
28
|
-
* 1.
|
|
29
|
-
*
|
|
25
|
+
* 1. When `claudePreToolUseGuard` is set, installs the managed Claude
|
|
26
|
+
* PreToolUse guard via the transactional `installClaudePreToolUse`
|
|
27
|
+
* (SkillGuard Slice 4a). The legacy copy-if-absent
|
|
28
|
+
* `claude-settings-security.json` scaffold is RETIRED — the managed
|
|
29
|
+
* installer owns `.claude/settings.json` + the hook asset with proper
|
|
30
|
+
* ownership markers.
|
|
30
31
|
* 2. Merges the `hooks:` security sections for the selected reliability
|
|
31
32
|
* profile into `.javi-forge/ci.yaml` via `setHookFeature` (creating a
|
|
32
33
|
* minimal `version: 2` config when absent). The dispatcher composes these
|
|
@@ -37,7 +38,7 @@ const PROFILE_PRESET = {
|
|
|
37
38
|
*/
|
|
38
39
|
export const stepSecurityHooks = async (ctx) => {
|
|
39
40
|
const { projectDir, dryRun, onStep, options } = ctx;
|
|
40
|
-
const { securityHooks, hookProfile } = options;
|
|
41
|
+
const { securityHooks, hookProfile, claudePreToolUseGuard } = options;
|
|
41
42
|
const stepId = "security-hooks";
|
|
42
43
|
report(onStep, stepId, "Scaffold security hooks", "running");
|
|
43
44
|
try {
|
|
@@ -48,18 +49,22 @@ export const stepSecurityHooks = async (ctx) => {
|
|
|
48
49
|
const profile = hookProfile ?? "standard";
|
|
49
50
|
const preset = PROFILE_PRESET[profile];
|
|
50
51
|
if (dryRun) {
|
|
51
|
-
|
|
52
|
+
const guardNote = claudePreToolUseGuard
|
|
53
|
+
? " + install Claude PreToolUse guard"
|
|
54
|
+
: "";
|
|
55
|
+
report(onStep, stepId, "Scaffold security hooks", "done", `dry-run: would merge ${profile} hooks preset${guardNote}`);
|
|
52
56
|
return;
|
|
53
57
|
}
|
|
54
|
-
// 1.
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
await
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
58
|
+
// 1. Install the managed Claude PreToolUse guard (transactional; owns
|
|
59
|
+
// .claude/settings.json + the hook asset). Retires the legacy copy.
|
|
60
|
+
let guardNote = "";
|
|
61
|
+
if (claudePreToolUseGuard) {
|
|
62
|
+
const result = await installClaudePreToolUse(projectDir);
|
|
63
|
+
if (!result.ok) {
|
|
64
|
+
report(onStep, stepId, "Scaffold security hooks", "error", `Claude guard install refused: ${result.errors.join("; ")}`);
|
|
65
|
+
return;
|
|
62
66
|
}
|
|
67
|
+
guardNote = "; Claude guard installed";
|
|
63
68
|
}
|
|
64
69
|
// 2. Merge the profile's security sections into .javi-forge/ci.yaml.
|
|
65
70
|
for (const feature of preset.preCommit) {
|
|
@@ -72,9 +77,10 @@ export const stepSecurityHooks = async (ctx) => {
|
|
|
72
77
|
...preset.preCommit.map((f) => `pre-commit.${f}`),
|
|
73
78
|
...preset.prePush.map((f) => `pre-push.${f}`),
|
|
74
79
|
];
|
|
75
|
-
|
|
80
|
+
const presetNote = merged.length > 0
|
|
76
81
|
? `${profile} preset: ${merged.join(", ")}`
|
|
77
|
-
: `${profile} preset: CI gate only (no security sections)
|
|
82
|
+
: `${profile} preset: CI gate only (no security sections)`;
|
|
83
|
+
report(onStep, stepId, "Scaffold security hooks", "done", `${presetNote}${guardNote}`);
|
|
78
84
|
}
|
|
79
85
|
catch (e) {
|
|
80
86
|
report(onStep, stepId, "Scaffold security hooks", "error", String(e));
|
|
@@ -51,6 +51,7 @@ export interface ClaudeHookDoctorReport {
|
|
|
51
51
|
coverage: typeof COVERAGE;
|
|
52
52
|
hostResidual: string;
|
|
53
53
|
remediation: readonly string[];
|
|
54
|
+
execution: ExecutionReport;
|
|
54
55
|
}
|
|
55
56
|
/**
|
|
56
57
|
* Classify the asset into one of nine states from observed bytes only. Never
|
|
@@ -66,14 +67,100 @@ export declare function detectNode(nodeVersion: string | undefined): {
|
|
|
66
67
|
version?: string;
|
|
67
68
|
satisfiesMinimum: boolean;
|
|
68
69
|
};
|
|
70
|
+
/**
|
|
71
|
+
* The honest effective-execution verdict. `status` is derived by precedence
|
|
72
|
+
* (blocked > inconclusive > runnable); `runnable` is reached only when every
|
|
73
|
+
* relevant local source read clear AND the managed guard is current. `residual`
|
|
74
|
+
* carries CONSTANT honest caveats (server-delivered policy, session safe-mode)
|
|
75
|
+
* that are always shown but never gate the status — otherwise `runnable` would
|
|
76
|
+
* be unreachable.
|
|
77
|
+
*/
|
|
78
|
+
export interface ExecutionReport {
|
|
79
|
+
status: "runnable" | "blocked" | "inconclusive";
|
|
80
|
+
blockers: string[];
|
|
81
|
+
unknownSources: string[];
|
|
82
|
+
residual: string[];
|
|
83
|
+
}
|
|
84
|
+
/** Injectable seams so units never hard-read real `/etc` or `/Library`. */
|
|
85
|
+
export interface ExecutionProbeEnv {
|
|
86
|
+
platform?: NodeJS.Platform;
|
|
87
|
+
homeDir?: string;
|
|
88
|
+
env?: NodeJS.ProcessEnv;
|
|
89
|
+
/** Override the managed OS file (null → no managed file at all). */
|
|
90
|
+
managedFile?: string | null;
|
|
91
|
+
/** Override the managed drop-in dir (null → no drop-ins). */
|
|
92
|
+
managedDropInDir?: string | null;
|
|
93
|
+
/** Override the drop-in directory listing (defaults to a confined readdir). */
|
|
94
|
+
listDir?: (dir: string) => Promise<string[]>;
|
|
95
|
+
}
|
|
96
|
+
/** Per-source read outcome; never promotes an unobservable source to clear. */
|
|
97
|
+
export type ExecutionSourceProbe = {
|
|
98
|
+
kind: "clear";
|
|
99
|
+
} | {
|
|
100
|
+
kind: "blocking";
|
|
101
|
+
flag: "disableAllHooks" | "allowManagedHooksOnly";
|
|
102
|
+
} | {
|
|
103
|
+
kind: "unknown";
|
|
104
|
+
reason: string;
|
|
105
|
+
};
|
|
106
|
+
/** The already-computed component states the guard-currency check consumes. */
|
|
107
|
+
export interface ExecutionComponentStates {
|
|
108
|
+
asset: ClaudeHookComponentState;
|
|
109
|
+
settings: ClaudeHookComponentState;
|
|
110
|
+
}
|
|
111
|
+
export interface ManagedSettingsPaths {
|
|
112
|
+
file: string;
|
|
113
|
+
dropInDir: string;
|
|
114
|
+
}
|
|
115
|
+
/** Static managed-settings locations per OS (no fs). WSL reports `linux`. */
|
|
116
|
+
export declare function resolveManagedSettingsPaths(platform: NodeJS.Platform): ManagedSettingsPaths;
|
|
117
|
+
/**
|
|
118
|
+
* Probe one settings source path for the two documented hook-neutralizing
|
|
119
|
+
* flags. Fail-closed: only a genuinely-absent path or a cleanly-parsed source
|
|
120
|
+
* with no flag is `clear`; a symlink, non-regular path, permission/io error,
|
|
121
|
+
* oversized/binary content, or malformed JSON is `unknown` (unreadable ≠
|
|
122
|
+
* absent), never `clear`. `disableAllHooks` is preferred over
|
|
123
|
+
* `allowManagedHooksOnly` when both are set (the former blocks at any source).
|
|
124
|
+
*/
|
|
125
|
+
export declare function probeExecutionSource(target: string): Promise<ExecutionSourceProbe>;
|
|
126
|
+
/**
|
|
127
|
+
* Confined readdir result of the managed drop-in dir, fail-closed and mirroring
|
|
128
|
+
* `probeExecutionSource`'s errno classification: a genuinely-absent directory
|
|
129
|
+
* (ENOENT/ENOTDIR) is the ONLY empty/clear case, so it yields `{ entries: [] }`
|
|
130
|
+
* (no drop-ins). ANY other readdir failure (EACCES/EIO/ELOOP/…) means the
|
|
131
|
+
* directory is present-but-unenumerable and MUST NOT be treated as empty — it
|
|
132
|
+
* yields `{ unreadable: true }` so the caller can degrade the verdict, never
|
|
133
|
+
* silently report "no drop-ins" (a false `runnable`).
|
|
134
|
+
*/
|
|
135
|
+
export type ManagedDropInListing = {
|
|
136
|
+
unreadable?: false;
|
|
137
|
+
entries: string[];
|
|
138
|
+
} | {
|
|
139
|
+
unreadable: true;
|
|
140
|
+
reason: string;
|
|
141
|
+
};
|
|
142
|
+
export declare function listManagedDropIns(dir: string, listDir?: (dir: string) => Promise<string[]>): Promise<ManagedDropInListing>;
|
|
143
|
+
/**
|
|
144
|
+
* The fail-closed effective-execution verdict. Gathers all sources in stable
|
|
145
|
+
* order (project, local, user, managed OS file, drop-ins sorted), classifies
|
|
146
|
+
* each with `probeExecutionSource`, applies the managed-only inertness of
|
|
147
|
+
* `allowManagedHooksOnly` (blocks only from a managed source — hooks MERGE
|
|
148
|
+
* elsewhere), folds in the guard-currency blocker and the doctor-process
|
|
149
|
+
* safe-mode observation, then resolves by precedence
|
|
150
|
+
* (`blockers` first, then `unknownSources`, else `runnable`). An `unknown` is
|
|
151
|
+
* NEVER promoted to `runnable`.
|
|
152
|
+
*/
|
|
153
|
+
export declare function probeExecution(projectDir: string, componentStates: ExecutionComponentStates, env?: ExecutionProbeEnv): Promise<ExecutionReport>;
|
|
69
154
|
/**
|
|
70
155
|
* Assemble the read-only component-level doctor report (no writes). `healthy` is
|
|
71
156
|
* exactly: both components `managed-current`, matcher and command shape exact,
|
|
72
157
|
* Node `>=22`. `assetSettingsConsistent` is a reported advisory, NOT part of it.
|
|
158
|
+
* The `execution` verdict is INDEPENDENT of `healthy`.
|
|
73
159
|
*/
|
|
74
160
|
export declare function doctorClaudePreToolUse(projectDir: string, options?: {
|
|
75
161
|
manifest?: Manifest;
|
|
76
162
|
nodeVersion?: string;
|
|
163
|
+
execution?: ExecutionProbeEnv;
|
|
77
164
|
}): Promise<ClaudeHookDoctorReport>;
|
|
78
165
|
export interface ClaudeHookMutationResult {
|
|
79
166
|
ok: boolean;
|
|
@@ -8,11 +8,12 @@
|
|
|
8
8
|
* Slice-3 seams — Slice 3 GROWS this file, it does not relocate this code.
|
|
9
9
|
*/
|
|
10
10
|
import { createHash, randomBytes } from "node:crypto";
|
|
11
|
-
import { lstat, readFile } from "node:fs/promises";
|
|
11
|
+
import { lstat, readdir, readFile } from "node:fs/promises";
|
|
12
|
+
import os from "node:os";
|
|
12
13
|
import path from "node:path";
|
|
13
14
|
import { CLAUDE_HOOK_ASSETS_DIR } from "../constants.js";
|
|
14
15
|
import { ASSET_MANAGED_MARKER, ASSET_NAME, } from "./__fixtures__/claude-hook-ownership.js";
|
|
15
|
-
import { buildManagedContainer, classifySettingsEntry, isPlainObject, LEGACY_FILE_SHA256, MANAGED_ASSET_ARG, MANAGED_MATCHER, MANAGED_STATUS_PREFIX, planForceReplace, planLegacyCohortExcision, planManagedClaudeHookMerge, } from "./claude-hook-settings.js";
|
|
16
|
+
import { buildManagedContainer, classifySettingsEntry, isPlainObject, LEGACY_FILE_SHA256, MANAGED_ASSET_ARG, MANAGED_MATCHER, MANAGED_STATUS_PREFIX, planForceReplace, planLegacyCohortExcision, planManagedClaudeHookMerge, scanExecutionFlags, } from "./claude-hook-settings.js";
|
|
16
17
|
import { safeReadFile } from "./safe-read.js";
|
|
17
18
|
import { selectSecureFs } from "./secure-fs-posix.js";
|
|
18
19
|
import { runTransaction, } from "./secure-fs-transaction.js";
|
|
@@ -200,10 +201,201 @@ const REMEDIATION = {
|
|
|
200
201
|
function remediationFor(state, component) {
|
|
201
202
|
return REMEDIATION[state]?.replace("$", component);
|
|
202
203
|
}
|
|
204
|
+
/** Static managed-settings locations per OS (no fs). WSL reports `linux`. */
|
|
205
|
+
export function resolveManagedSettingsPaths(platform) {
|
|
206
|
+
if (platform === "darwin") {
|
|
207
|
+
const base = "/Library/Application Support/ClaudeCode";
|
|
208
|
+
return {
|
|
209
|
+
file: `${base}/managed-settings.json`,
|
|
210
|
+
dropInDir: `${base}/managed-settings.d`,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
if (platform === "win32") {
|
|
214
|
+
const base = "C:\\Program Files\\ClaudeCode";
|
|
215
|
+
return {
|
|
216
|
+
file: `${base}\\managed-settings.json`,
|
|
217
|
+
dropInDir: `${base}\\managed-settings.d`,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
const base = "/etc/claude-code";
|
|
221
|
+
return {
|
|
222
|
+
file: `${base}/managed-settings.json`,
|
|
223
|
+
dropInDir: `${base}/managed-settings.d`,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Probe one settings source path for the two documented hook-neutralizing
|
|
228
|
+
* flags. Fail-closed: only a genuinely-absent path or a cleanly-parsed source
|
|
229
|
+
* with no flag is `clear`; a symlink, non-regular path, permission/io error,
|
|
230
|
+
* oversized/binary content, or malformed JSON is `unknown` (unreadable ≠
|
|
231
|
+
* absent), never `clear`. `disableAllHooks` is preferred over
|
|
232
|
+
* `allowManagedHooksOnly` when both are set (the former blocks at any source).
|
|
233
|
+
*/
|
|
234
|
+
export async function probeExecutionSource(target) {
|
|
235
|
+
const stat = await lstatNoFollow(target);
|
|
236
|
+
if (stat.kind === "enoent")
|
|
237
|
+
return { kind: "clear" };
|
|
238
|
+
if (stat.kind === "symlink")
|
|
239
|
+
return { kind: "unknown", reason: "symlink" };
|
|
240
|
+
if (stat.kind === "non-regular") {
|
|
241
|
+
return { kind: "unknown", reason: "non-regular" };
|
|
242
|
+
}
|
|
243
|
+
if (stat.kind === "error")
|
|
244
|
+
return { kind: "unknown", reason: stat.detail };
|
|
245
|
+
const read = await safeReadFile(target, READ_OPTS);
|
|
246
|
+
if (!read.ok) {
|
|
247
|
+
if (read.reason === "not-found")
|
|
248
|
+
return { kind: "clear" };
|
|
249
|
+
return { kind: "unknown", reason: read.reason };
|
|
250
|
+
}
|
|
251
|
+
let parsed;
|
|
252
|
+
try {
|
|
253
|
+
parsed = JSON.parse(read.content);
|
|
254
|
+
}
|
|
255
|
+
catch {
|
|
256
|
+
return { kind: "unknown", reason: "invalid-json" };
|
|
257
|
+
}
|
|
258
|
+
const flags = scanExecutionFlags(parsed);
|
|
259
|
+
if (flags.disableAllHooks) {
|
|
260
|
+
return { kind: "blocking", flag: "disableAllHooks" };
|
|
261
|
+
}
|
|
262
|
+
if (flags.allowManagedHooksOnly) {
|
|
263
|
+
return { kind: "blocking", flag: "allowManagedHooksOnly" };
|
|
264
|
+
}
|
|
265
|
+
return { kind: "clear" };
|
|
266
|
+
}
|
|
267
|
+
export async function listManagedDropIns(dir, listDir) {
|
|
268
|
+
let entries;
|
|
269
|
+
try {
|
|
270
|
+
entries = listDir ? await listDir(dir) : await readdir(dir);
|
|
271
|
+
}
|
|
272
|
+
catch (error) {
|
|
273
|
+
const code = error.code;
|
|
274
|
+
// Only a genuinely-absent directory is "no drop-ins"; every other error
|
|
275
|
+
// (permission/io/loop/etc.) is an unreadable managed source.
|
|
276
|
+
if (code === "ENOENT" || code === "ENOTDIR")
|
|
277
|
+
return { entries: [] };
|
|
278
|
+
return { unreadable: true, reason: code ?? String(error) };
|
|
279
|
+
}
|
|
280
|
+
return {
|
|
281
|
+
entries: entries
|
|
282
|
+
.filter((name) => name.endsWith(".json"))
|
|
283
|
+
.sort()
|
|
284
|
+
.map((name) => path.join(dir, name)),
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
/** CONSTANT honest limits — always rendered, never gate the status. */
|
|
288
|
+
const EXECUTION_RESIDUAL = [
|
|
289
|
+
"server-delivered managed policy can disable hooks and is not observable from local files",
|
|
290
|
+
"session safe-mode (--safe-mode / CLAUDE_CODE_SAFE_MODE) in the diagnosed session is not observable from this process",
|
|
291
|
+
];
|
|
292
|
+
function isSafeModeTruthy(env) {
|
|
293
|
+
const value = env.CLAUDE_CODE_SAFE_MODE;
|
|
294
|
+
if (value === undefined)
|
|
295
|
+
return false;
|
|
296
|
+
const normalized = value.trim().toLowerCase();
|
|
297
|
+
return normalized !== "" && normalized !== "0" && normalized !== "false";
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* The fail-closed effective-execution verdict. Gathers all sources in stable
|
|
301
|
+
* order (project, local, user, managed OS file, drop-ins sorted), classifies
|
|
302
|
+
* each with `probeExecutionSource`, applies the managed-only inertness of
|
|
303
|
+
* `allowManagedHooksOnly` (blocks only from a managed source — hooks MERGE
|
|
304
|
+
* elsewhere), folds in the guard-currency blocker and the doctor-process
|
|
305
|
+
* safe-mode observation, then resolves by precedence
|
|
306
|
+
* (`blockers` first, then `unknownSources`, else `runnable`). An `unknown` is
|
|
307
|
+
* NEVER promoted to `runnable`.
|
|
308
|
+
*/
|
|
309
|
+
export async function probeExecution(projectDir, componentStates, env = {}) {
|
|
310
|
+
const platform = env.platform ?? process.platform;
|
|
311
|
+
const homeDir = env.homeDir ?? os.homedir();
|
|
312
|
+
const processEnv = env.env ?? process.env;
|
|
313
|
+
const resolved = resolveManagedSettingsPaths(platform);
|
|
314
|
+
const managedFile = env.managedFile !== undefined ? env.managedFile : resolved.file;
|
|
315
|
+
const dropInDir = env.managedDropInDir !== undefined
|
|
316
|
+
? env.managedDropInDir
|
|
317
|
+
: resolved.dropInDir;
|
|
318
|
+
const specs = [
|
|
319
|
+
{
|
|
320
|
+
label: "project",
|
|
321
|
+
target: path.join(projectDir, ".claude", "settings.json"),
|
|
322
|
+
managed: false,
|
|
323
|
+
},
|
|
324
|
+
{
|
|
325
|
+
label: "local",
|
|
326
|
+
target: path.join(projectDir, ".claude", "settings.local.json"),
|
|
327
|
+
managed: false,
|
|
328
|
+
},
|
|
329
|
+
{
|
|
330
|
+
label: "user",
|
|
331
|
+
target: path.join(homeDir, ".claude", "settings.json"),
|
|
332
|
+
managed: false,
|
|
333
|
+
},
|
|
334
|
+
];
|
|
335
|
+
if (managedFile) {
|
|
336
|
+
specs.push({ label: "managed", target: managedFile, managed: true });
|
|
337
|
+
}
|
|
338
|
+
// A present-but-unenumerable drop-in dir is an `unknown` managed source, NOT
|
|
339
|
+
// "no drop-ins" — fold it into unknownSources so a blocking drop-in policy we
|
|
340
|
+
// cannot read can never be mistaken for a clear (false `runnable`) verdict.
|
|
341
|
+
let dropInDirUnknown;
|
|
342
|
+
if (dropInDir) {
|
|
343
|
+
const listing = await listManagedDropIns(dropInDir, env.listDir);
|
|
344
|
+
if (listing.unreadable) {
|
|
345
|
+
dropInDirUnknown = `managed:${dropInDir} (${listing.reason})`;
|
|
346
|
+
}
|
|
347
|
+
else {
|
|
348
|
+
for (const target of listing.entries) {
|
|
349
|
+
specs.push({ label: "managed", target, managed: true });
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
const blockers = [];
|
|
354
|
+
const unknownSources = [];
|
|
355
|
+
for (const spec of specs) {
|
|
356
|
+
const probe = await probeExecutionSource(spec.target);
|
|
357
|
+
if (probe.kind === "clear")
|
|
358
|
+
continue;
|
|
359
|
+
if (probe.kind === "unknown") {
|
|
360
|
+
unknownSources.push(`${spec.label}:${spec.target} (${probe.reason})`);
|
|
361
|
+
continue;
|
|
362
|
+
}
|
|
363
|
+
// `allowManagedHooksOnly` is inert outside a managed source (hooks merge).
|
|
364
|
+
if (probe.flag === "allowManagedHooksOnly" && !spec.managed)
|
|
365
|
+
continue;
|
|
366
|
+
blockers.push(`policy:${probe.flag}@${spec.label}`);
|
|
367
|
+
}
|
|
368
|
+
if (dropInDirUnknown)
|
|
369
|
+
unknownSources.push(dropInDirUnknown);
|
|
370
|
+
// Guard-currency: a not-installed / drifted guard cannot fire, so it blocks.
|
|
371
|
+
if (componentStates.asset !== "managed-current") {
|
|
372
|
+
blockers.push(`guard:asset=${componentStates.asset}`);
|
|
373
|
+
}
|
|
374
|
+
if (componentStates.settings !== "managed-current") {
|
|
375
|
+
blockers.push(`guard:settings=${componentStates.settings}`);
|
|
376
|
+
}
|
|
377
|
+
// Safe-mode observed in THIS doctor process is a real per-run unknown (the
|
|
378
|
+
// diagnosed session's own safe-mode remains a constant residual).
|
|
379
|
+
if (isSafeModeTruthy(processEnv)) {
|
|
380
|
+
unknownSources.push("safe-mode:CLAUDE_CODE_SAFE_MODE (observed in doctor process only)");
|
|
381
|
+
}
|
|
382
|
+
const status = blockers.length > 0
|
|
383
|
+
? "blocked"
|
|
384
|
+
: unknownSources.length > 0
|
|
385
|
+
? "inconclusive"
|
|
386
|
+
: "runnable";
|
|
387
|
+
return {
|
|
388
|
+
status,
|
|
389
|
+
blockers,
|
|
390
|
+
unknownSources,
|
|
391
|
+
residual: [...EXECUTION_RESIDUAL],
|
|
392
|
+
};
|
|
393
|
+
}
|
|
203
394
|
/**
|
|
204
395
|
* Assemble the read-only component-level doctor report (no writes). `healthy` is
|
|
205
396
|
* exactly: both components `managed-current`, matcher and command shape exact,
|
|
206
397
|
* Node `>=22`. `assetSettingsConsistent` is a reported advisory, NOT part of it.
|
|
398
|
+
* The `execution` verdict is INDEPENDENT of `healthy`.
|
|
207
399
|
*/
|
|
208
400
|
export async function doctorClaudePreToolUse(projectDir, options) {
|
|
209
401
|
const manifest = options?.manifest ?? (await readManifest());
|
|
@@ -240,6 +432,7 @@ export async function doctorClaudePreToolUse(projectDir, options) {
|
|
|
240
432
|
signals.matcherExact &&
|
|
241
433
|
signals.commandShapeExact &&
|
|
242
434
|
node.satisfiesMinimum;
|
|
435
|
+
const execution = await probeExecution(projectDir, { asset: asset.state, settings: settings.state }, options?.execution ?? {});
|
|
243
436
|
return {
|
|
244
437
|
healthy,
|
|
245
438
|
settings: {
|
|
@@ -261,6 +454,7 @@ export async function doctorClaudePreToolUse(projectDir, options) {
|
|
|
261
454
|
coverage: COVERAGE,
|
|
262
455
|
hostResidual: HOST_RESIDUAL,
|
|
263
456
|
remediation: [...remediation].sort(),
|
|
457
|
+
execution,
|
|
264
458
|
};
|
|
265
459
|
}
|
|
266
460
|
async function readManifest() {
|
|
@@ -40,6 +40,24 @@ export declare function isPlainObject(value: unknown): value is Record<string, u
|
|
|
40
40
|
* else is malformed.
|
|
41
41
|
*/
|
|
42
42
|
export declare function validateSettingsShape(parsed: unknown): boolean;
|
|
43
|
+
/**
|
|
44
|
+
* The two documented scalar flags a settings source can set that neutralize the
|
|
45
|
+
* managed hook. `disableAllHooks: true` at ANY source is a blocker; the
|
|
46
|
+
* source→blocker mapping for `allowManagedHooksOnly` (managed-only) lives in the
|
|
47
|
+
* manager, which knows each source's provenance.
|
|
48
|
+
*/
|
|
49
|
+
export interface ExecutionFlagScan {
|
|
50
|
+
disableAllHooks: boolean;
|
|
51
|
+
allowManagedHooksOnly: boolean;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Classify an already-parsed settings container for the two documented
|
|
55
|
+
* hook-neutralizing flags. Strict `=== true` only: a truthy-but-not-`true`
|
|
56
|
+
* value (`"true"`, `1`), a missing key, or a non-object input is NOT a flag.
|
|
57
|
+
* A false here is a definitive "this source does not set the flag", never an
|
|
58
|
+
* "unknown" — unreadability is decided upstream by the fs probe, not here.
|
|
59
|
+
*/
|
|
60
|
+
export declare function scanExecutionFlags(parsed: unknown): ExecutionFlagScan;
|
|
43
61
|
/**
|
|
44
62
|
* Replace the trailing 64-hex asset SHA token in a managed `statusMessage` with
|
|
45
63
|
* the fixed placeholder so settings identity is invariant under asset rotation.
|
|
@@ -31,6 +31,22 @@ export function validateSettingsShape(parsed) {
|
|
|
31
31
|
return false;
|
|
32
32
|
return true;
|
|
33
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* Classify an already-parsed settings container for the two documented
|
|
36
|
+
* hook-neutralizing flags. Strict `=== true` only: a truthy-but-not-`true`
|
|
37
|
+
* value (`"true"`, `1`), a missing key, or a non-object input is NOT a flag.
|
|
38
|
+
* A false here is a definitive "this source does not set the flag", never an
|
|
39
|
+
* "unknown" — unreadability is decided upstream by the fs probe, not here.
|
|
40
|
+
*/
|
|
41
|
+
export function scanExecutionFlags(parsed) {
|
|
42
|
+
if (!isPlainObject(parsed)) {
|
|
43
|
+
return { disableAllHooks: false, allowManagedHooksOnly: false };
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
disableAllHooks: parsed.disableAllHooks === true,
|
|
47
|
+
allowManagedHooksOnly: parsed.allowManagedHooksOnly === true,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
34
50
|
// Canonical identity (Decision ②)
|
|
35
51
|
const ASSET_SHA_TOKEN = /^[0-9a-f]{64}$/;
|
|
36
52
|
const VERSION_PATTERN = /^javi-forge-global-pretooluse:v(\d+):sha256:/;
|
package/dist/types/index.d.ts
CHANGED
|
@@ -16,6 +16,11 @@ export interface InitOptions {
|
|
|
16
16
|
claudeMd: boolean;
|
|
17
17
|
securityHooks: boolean;
|
|
18
18
|
hookProfile: HookProfile;
|
|
19
|
+
/**
|
|
20
|
+
* Install the managed Claude PreToolUse guard during init. Derived from
|
|
21
|
+
* `securityHooks` at the single App.tsx call site (all profiles incl. Minimal).
|
|
22
|
+
*/
|
|
23
|
+
claudePreToolUseGuard: boolean;
|
|
19
24
|
codeGraph: boolean;
|
|
20
25
|
dockerDeploy: boolean;
|
|
21
26
|
/** Service name for docker rollout (default: 'app') */
|
package/dist/ui/App.js
CHANGED
|
@@ -93,6 +93,9 @@ export default function App({ dryRun = false, presetStack, presetCI, presetMemor
|
|
|
93
93
|
claudeMd: opts.claudeMd,
|
|
94
94
|
securityHooks: opts.securityHooks,
|
|
95
95
|
hookProfile: opts.hookProfile,
|
|
96
|
+
// Derived from securityHooks alone — ALL profiles incl. Minimal
|
|
97
|
+
// install the managed guard when security hooks are enabled.
|
|
98
|
+
claudePreToolUseGuard: opts.securityHooks,
|
|
96
99
|
codeGraph: opts.codeGraph,
|
|
97
100
|
localAi: opts.localAi,
|
|
98
101
|
dockerDeploy: false,
|