javi-forge 1.32.0 → 1.34.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 +2 -0
- package/dist/cli/help.d.ts +1 -1
- package/dist/cli/help.js +6 -0
- package/dist/commands/claude-hooks.d.ts +10 -6
- package/dist/commands/claude-hooks.js +73 -11
- package/dist/commands/init/steps/security.d.ts +2 -0
- package/dist/commands/init/steps/security.js +37 -7
- package/dist/lib/claude-hook-manager.d.ts +147 -0
- package/dist/lib/claude-hook-manager.js +337 -4
- package/dist/lib/claude-hook-settings.d.ts +32 -0
- package/dist/lib/claude-hook-settings.js +40 -0
- package/dist/lib/secure-fs-posix.d.ts +43 -0
- package/dist/lib/secure-fs-posix.js +66 -7
- package/dist/lib/secure-refusal-remediation.d.ts +30 -0
- package/dist/lib/secure-refusal-remediation.js +48 -0
- package/dist/ui/App.js +3 -16
- package/dist/ui/build-init-options.d.ts +44 -0
- package/dist/ui/build-init-options.js +39 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -375,6 +375,8 @@ npx javi-forge doctor
|
|
|
375
375
|
## Requirements
|
|
376
376
|
|
|
377
377
|
- **Node.js** >= 22 (required by ink 7; previous versions ran on >= 18)
|
|
378
|
+
- **Linux only** — the `acl` package (provides `getfacl`) is required to install or repair the Claude PreToolUse guard: the transactional installer proves every controlling directory carries no extended ACL, and refuses fail-closed when `getfacl` is unresolvable. Install with `apt install acl`, `apk add acl`, or `dnf install acl` (slim container images usually omit it). An already-installed guard keeps working without it — `javi-forge hooks doctor claude` reports the capability as its own row.
|
|
379
|
+
- **`node` on `PATH`** — Claude Code spawns the guard in exec form, so `node` must resolve on the `PATH` Claude Code itself uses, not only inside javi-forge.
|
|
378
380
|
|
|
379
381
|
## Ecosystem
|
|
380
382
|
|
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 $ 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";
|
|
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 Linux: install/repair claude need the acl package (getfacl) to prove the\n parent chain \u2014 apt install acl / apk add acl / dnf install acl. Without it\n they refuse fail-closed; an already-installed guard keeps firing, and\n doctor claude reports the acl capability as its own row.\n Claude Code spawns the guard with node from ITS path, so node must resolve\n there, not only inside javi-forge.\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
|
@@ -186,6 +186,12 @@ export const HOOKS_HELP_TEXT = `
|
|
|
186
186
|
doctor claude is informational (always exits 0); install/repair exit 0 on
|
|
187
187
|
success, non-zero on refusal/failure. Use repair claude --force to overwrite
|
|
188
188
|
a locally edited managed asset.
|
|
189
|
+
Linux: install/repair claude need the acl package (getfacl) to prove the
|
|
190
|
+
parent chain — apt install acl / apk add acl / dnf install acl. Without it
|
|
191
|
+
they refuse fail-closed; an already-installed guard keeps firing, and
|
|
192
|
+
doctor claude reports the acl capability as its own row.
|
|
193
|
+
Claude Code spawns the guard with node from ITS path, so node must resolve
|
|
194
|
+
there, not only inside javi-forge.
|
|
189
195
|
|
|
190
196
|
Examples
|
|
191
197
|
$ javi-forge hooks run pre-commit
|
|
@@ -4,11 +4,14 @@
|
|
|
4
4
|
* three lib fns to human output + exit codes; it adds NO new security logic and
|
|
5
5
|
* never touches `runTransaction`/secure-fs directly.
|
|
6
6
|
*
|
|
7
|
-
* Honest-execution
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
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.
|
|
12
15
|
*/
|
|
13
16
|
import { doctorClaudePreToolUse, installClaudePreToolUse, repairClaudePreToolUse } from "../lib/claude-hook-manager.js";
|
|
14
17
|
export type ClaudeHookSub = "install" | "doctor" | "repair";
|
|
@@ -24,7 +27,8 @@ export interface ClaudeHookCmdDeps {
|
|
|
24
27
|
* Run one Claude-hook subcommand against `projectDir`. Returns the process exit
|
|
25
28
|
* code (the dispatcher calls `process.exit`, not this fn):
|
|
26
29
|
* - install/repair: 0 when `ok`, 1 on refusal/failure.
|
|
27
|
-
* - doctor:
|
|
30
|
+
* - doctor: follows `execution.status` — runnable → 0, blocked → 1,
|
|
31
|
+
* inconclusive → 2.
|
|
28
32
|
*/
|
|
29
33
|
export declare function runClaudeHookCommand(sub: ClaudeHookSub, projectDir: string, opts: {
|
|
30
34
|
force?: boolean;
|
|
@@ -4,13 +4,28 @@
|
|
|
4
4
|
* three lib fns to human output + exit codes; it adds NO new security logic and
|
|
5
5
|
* never touches `runTransaction`/secure-fs directly.
|
|
6
6
|
*
|
|
7
|
-
* Honest-execution
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
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.
|
|
12
15
|
*/
|
|
13
16
|
import { doctorClaudePreToolUse, installClaudePreToolUse, repairClaudePreToolUse, } from "../lib/claude-hook-manager.js";
|
|
17
|
+
import { remediationForMessage } from "../lib/secure-refusal-remediation.js";
|
|
18
|
+
/**
|
|
19
|
+
* Warnings are NON-BLOCKING notices, so they go to stdout under their own
|
|
20
|
+
* heading — never to the error stream and never into the exit code.
|
|
21
|
+
*/
|
|
22
|
+
function renderWarnings(warnings, log) {
|
|
23
|
+
if (warnings.length === 0)
|
|
24
|
+
return;
|
|
25
|
+
log("warnings:");
|
|
26
|
+
for (const w of warnings)
|
|
27
|
+
log(` ${w}`);
|
|
28
|
+
}
|
|
14
29
|
function renderMutation(verb, result, log, logError) {
|
|
15
30
|
if (result.ok) {
|
|
16
31
|
log(`${verb} claude: ok`);
|
|
@@ -27,11 +42,19 @@ function renderMutation(verb, result, log, logError) {
|
|
|
27
42
|
for (const p of result.backups)
|
|
28
43
|
log(` ${p}`);
|
|
29
44
|
}
|
|
45
|
+
renderWarnings(result.warnings, log);
|
|
30
46
|
return 0;
|
|
31
47
|
}
|
|
32
48
|
logError(`${verb} claude: refused`);
|
|
33
|
-
|
|
49
|
+
// A refusal whose detail maps to a remediation is never rendered bare: the
|
|
50
|
+
// mapping is a CLI-layer lookup, so the adapter's refusal codes stay stable.
|
|
51
|
+
for (const e of result.errors) {
|
|
34
52
|
logError(` ${e}`);
|
|
53
|
+
const remediation = remediationForMessage(e);
|
|
54
|
+
if (remediation)
|
|
55
|
+
logError(` → ${remediation}`);
|
|
56
|
+
}
|
|
57
|
+
renderWarnings(result.warnings, log);
|
|
35
58
|
return 1;
|
|
36
59
|
}
|
|
37
60
|
function renderDoctor(report, log) {
|
|
@@ -39,23 +62,62 @@ function renderDoctor(report, log) {
|
|
|
39
62
|
log(` settings: ${report.settings.state} — ${report.settings.detail}`);
|
|
40
63
|
log(` asset: ${report.asset.state} — ${report.asset.detail}`);
|
|
41
64
|
log(` node: ${report.node.version ?? "unavailable"} (min-satisfied: ${report.node.satisfiesMinimum})`);
|
|
42
|
-
//
|
|
43
|
-
|
|
65
|
+
// The node-on-PATH row is ALWAYS printed (satisfied included) and is always
|
|
66
|
+
// labelled a heuristic: this process' PATH only proxies the PATH Claude Code
|
|
67
|
+
// will use to spawn the exec-form handler.
|
|
68
|
+
const onPath = report.nodeOnPath;
|
|
69
|
+
const onPathDetail = onPath.status === "resolved"
|
|
70
|
+
? ` ${onPath.version}`
|
|
71
|
+
: onPath.status === "unknown"
|
|
72
|
+
? ` — ${onPath.detail}`
|
|
73
|
+
: "";
|
|
74
|
+
log(` node-on-PATH: ${onPath.status}${onPathDetail} (heuristic: this process' PATH)`);
|
|
75
|
+
const execution = report.execution;
|
|
76
|
+
log(` execution: ${execution.status}`);
|
|
77
|
+
if (execution.blockers.length > 0) {
|
|
78
|
+
log(" blockers:");
|
|
79
|
+
for (const b of execution.blockers)
|
|
80
|
+
log(` - ${b}`);
|
|
81
|
+
}
|
|
82
|
+
if (execution.unknownSources.length > 0) {
|
|
83
|
+
log(" unknown-sources:");
|
|
84
|
+
for (const u of execution.unknownSources)
|
|
85
|
+
log(` - ${u}`);
|
|
86
|
+
}
|
|
87
|
+
if (execution.residual.length > 0) {
|
|
88
|
+
log(" execution-residual:");
|
|
89
|
+
for (const r of execution.residual)
|
|
90
|
+
log(` - ${r}`);
|
|
91
|
+
}
|
|
92
|
+
// Install-capability is its OWN section, always printed so an absent adapter
|
|
93
|
+
// is never silent — and it never changes the exit code, which follows
|
|
94
|
+
// `execution.status` alone.
|
|
95
|
+
const acl = report.installCapability.acl;
|
|
96
|
+
const aclDetail = "detail" in acl ? ` — ${acl.detail}` : "";
|
|
97
|
+
log(` acl-capability: ${acl.status} (${acl.tool})${aclDetail}`);
|
|
98
|
+
if (report.installCapability.remediation) {
|
|
99
|
+
log(` → ${report.installCapability.remediation}`);
|
|
100
|
+
}
|
|
44
101
|
log(` host-residual: ${report.hostResidual}`);
|
|
45
102
|
if (report.remediation.length > 0) {
|
|
46
103
|
log(" remediation:");
|
|
47
104
|
for (const r of report.remediation)
|
|
48
105
|
log(` - ${r}`);
|
|
49
106
|
}
|
|
50
|
-
//
|
|
51
|
-
//
|
|
107
|
+
// The doctor exit code follows the effective-execution verdict, independent
|
|
108
|
+
// of component health: runnable → 0, blocked → 1, inconclusive → 2.
|
|
109
|
+
if (execution.status === "blocked")
|
|
110
|
+
return 1;
|
|
111
|
+
if (execution.status === "inconclusive")
|
|
112
|
+
return 2;
|
|
52
113
|
return 0;
|
|
53
114
|
}
|
|
54
115
|
/**
|
|
55
116
|
* Run one Claude-hook subcommand against `projectDir`. Returns the process exit
|
|
56
117
|
* code (the dispatcher calls `process.exit`, not this fn):
|
|
57
118
|
* - install/repair: 0 when `ok`, 1 on refusal/failure.
|
|
58
|
-
* - doctor:
|
|
119
|
+
* - doctor: follows `execution.status` — runnable → 0, blocked → 1,
|
|
120
|
+
* inconclusive → 2.
|
|
59
121
|
*/
|
|
60
122
|
export async function runClaudeHookCommand(sub, projectDir, opts, deps = {}) {
|
|
61
123
|
const log = deps.log ?? ((m) => console.log(m));
|
|
@@ -10,6 +10,8 @@ import type { StepFn } from "../types.js";
|
|
|
10
10
|
* `claude-settings-security.json` scaffold is RETIRED — the managed
|
|
11
11
|
* installer owns `.claude/settings.json` + the hook asset with proper
|
|
12
12
|
* ownership markers.
|
|
13
|
+
* A guard refusal/failure is REPORTED but does NOT abort the step: the
|
|
14
|
+
* profile merge below is an independent outcome (Linux hardening Slice A).
|
|
13
15
|
* 2. Merges the `hooks:` security sections for the selected reliability
|
|
14
16
|
* profile into `.javi-forge/ci.yaml` via `setHookFeature` (creating a
|
|
15
17
|
* minimal `version: 2` config when absent). The dispatcher composes these
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { setHookFeature } from "../../../lib/ci-config.js";
|
|
2
2
|
import { installClaudePreToolUse } from "../../../lib/claude-hook-manager.js";
|
|
3
|
+
import { remediationForMessage } from "../../../lib/secure-refusal-remediation.js";
|
|
3
4
|
import { report } from "../report.js";
|
|
4
5
|
/**
|
|
5
6
|
* Hook-feature preset per reliability profile (hook-consolidation S4).
|
|
@@ -28,6 +29,8 @@ const PROFILE_PRESET = {
|
|
|
28
29
|
* `claude-settings-security.json` scaffold is RETIRED — the managed
|
|
29
30
|
* installer owns `.claude/settings.json` + the hook asset with proper
|
|
30
31
|
* ownership markers.
|
|
32
|
+
* A guard refusal/failure is REPORTED but does NOT abort the step: the
|
|
33
|
+
* profile merge below is an independent outcome (Linux hardening Slice A).
|
|
31
34
|
* 2. Merges the `hooks:` security sections for the selected reliability
|
|
32
35
|
* profile into `.javi-forge/ci.yaml` via `setHookFeature` (creating a
|
|
33
36
|
* minimal `version: 2` config when absent). The dispatcher composes these
|
|
@@ -41,6 +44,11 @@ export const stepSecurityHooks = async (ctx) => {
|
|
|
41
44
|
const { securityHooks, hookProfile, claudePreToolUseGuard } = options;
|
|
42
45
|
const stepId = "security-hooks";
|
|
43
46
|
report(onStep, stepId, "Scaffold security hooks", "running");
|
|
47
|
+
// Declared OUTSIDE the try so a throw from the profile merge below cannot
|
|
48
|
+
// swallow a guard refusal that already happened: the outer catch reports
|
|
49
|
+
// BOTH failures (Linux hardening Slice A — a captured refusal and its
|
|
50
|
+
// remediation are never silently lost).
|
|
51
|
+
let guardError;
|
|
44
52
|
try {
|
|
45
53
|
if (!securityHooks) {
|
|
46
54
|
report(onStep, stepId, "Scaffold security hooks", "skipped", "not selected");
|
|
@@ -57,14 +65,30 @@ export const stepSecurityHooks = async (ctx) => {
|
|
|
57
65
|
}
|
|
58
66
|
// 1. Install the managed Claude PreToolUse guard (transactional; owns
|
|
59
67
|
// .claude/settings.json + the hook asset). Retires the legacy copy.
|
|
68
|
+
// A refusal (or a throw) is CAPTURED, not returned on: the guard and
|
|
69
|
+
// the hook-profile merge below are independent outcomes, so a host
|
|
70
|
+
// that cannot install the guard must not silently lose its
|
|
71
|
+
// secrets/permissions/deps wiring. The captured failure still drives a
|
|
72
|
+
// terminal status of "error" — visibility is never downgraded.
|
|
60
73
|
let guardNote = "";
|
|
61
74
|
if (claudePreToolUseGuard) {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
75
|
+
try {
|
|
76
|
+
const result = await installClaudePreToolUse(projectDir);
|
|
77
|
+
if (result.ok) {
|
|
78
|
+
guardNote = "; Claude guard installed";
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
guardError = `Claude guard install refused: ${result.errors.join("; ")}`;
|
|
82
|
+
const remediation = result.errors
|
|
83
|
+
.map((e) => remediationForMessage(e))
|
|
84
|
+
.find((line) => line !== undefined);
|
|
85
|
+
if (remediation)
|
|
86
|
+
guardError += ` → ${remediation}`;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
catch (e) {
|
|
90
|
+
guardError = `Claude guard install failed: ${String(e)}`;
|
|
66
91
|
}
|
|
67
|
-
guardNote = "; Claude guard installed";
|
|
68
92
|
}
|
|
69
93
|
// 2. Merge the profile's security sections into .javi-forge/ci.yaml.
|
|
70
94
|
for (const feature of preset.preCommit) {
|
|
@@ -80,10 +104,16 @@ export const stepSecurityHooks = async (ctx) => {
|
|
|
80
104
|
const presetNote = merged.length > 0
|
|
81
105
|
? `${profile} preset: ${merged.join(", ")}`
|
|
82
106
|
: `${profile} preset: CI gate only (no security sections)`;
|
|
83
|
-
report
|
|
107
|
+
// ONE terminal report. On a captured guard failure the status stays
|
|
108
|
+
// "error" and the detail names BOTH the refusal (+ remediation) AND the
|
|
109
|
+
// preset that WAS merged, so a refused guard is never read as installed
|
|
110
|
+
// and a merged profile is never read as lost.
|
|
111
|
+
report(onStep, stepId, "Scaffold security hooks", guardError ? "error" : "done", guardError
|
|
112
|
+
? `${guardError}; ${presetNote} merged`
|
|
113
|
+
: `${presetNote}${guardNote}`);
|
|
84
114
|
}
|
|
85
115
|
catch (e) {
|
|
86
|
-
report(onStep, stepId, "Scaffold security hooks", "error", String(e));
|
|
116
|
+
report(onStep, stepId, "Scaffold security hooks", "error", guardError ? `${guardError}; ${String(e)}` : String(e));
|
|
87
117
|
}
|
|
88
118
|
};
|
|
89
119
|
//# sourceMappingURL=security.js.map
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* Slice-3 seams — Slice 3 GROWS this file, it does not relocate this code.
|
|
9
9
|
*/
|
|
10
10
|
import { type ClaudeHookComponentState, type SettingsClassification, type SettingsIdentityManifest } from "./claude-hook-settings.js";
|
|
11
|
+
import { type AclCapability, type SpawnFn } from "./secure-fs-posix.js";
|
|
11
12
|
import { type PlatformSecureFs } from "./secure-fs-transaction.js";
|
|
12
13
|
declare const COVERAGE: readonly ["Bash", "PowerShell", "Read", "Write", "Edit"];
|
|
13
14
|
export interface AssetManifestEntry {
|
|
@@ -51,6 +52,24 @@ export interface ClaudeHookDoctorReport {
|
|
|
51
52
|
coverage: typeof COVERAGE;
|
|
52
53
|
hostResidual: string;
|
|
53
54
|
remediation: readonly string[];
|
|
55
|
+
execution: ExecutionReport;
|
|
56
|
+
/**
|
|
57
|
+
* Host INSTALL-capability, reported as its own section. It answers "could
|
|
58
|
+
* install/repair run here?", NOT "will the installed guard fire?" — so it
|
|
59
|
+
* never feeds `execution`, `healthy`, or the exit code. A current, firing
|
|
60
|
+
* guard on an image without `getfacl` stays `runnable`.
|
|
61
|
+
*/
|
|
62
|
+
installCapability: {
|
|
63
|
+
acl: AclCapability;
|
|
64
|
+
remediation?: string;
|
|
65
|
+
};
|
|
66
|
+
/**
|
|
67
|
+
* The node-on-PATH HEURISTIC row, always present so an absence is never
|
|
68
|
+
* silent. It is DISTINCT from `node` (which measures this process'
|
|
69
|
+
* `process.versions.node`) and it never inflates confidence: a `resolved`
|
|
70
|
+
* row clears no blocker and no unknown source.
|
|
71
|
+
*/
|
|
72
|
+
nodeOnPath: NodeOnPathProbe;
|
|
54
73
|
}
|
|
55
74
|
/**
|
|
56
75
|
* Classify the asset into one of nine states from observed bytes only. Never
|
|
@@ -66,14 +85,128 @@ export declare function detectNode(nodeVersion: string | undefined): {
|
|
|
66
85
|
version?: string;
|
|
67
86
|
satisfiesMinimum: boolean;
|
|
68
87
|
};
|
|
88
|
+
/**
|
|
89
|
+
* The honest effective-execution verdict. `status` is derived by precedence
|
|
90
|
+
* (blocked > inconclusive > runnable); `runnable` is reached only when every
|
|
91
|
+
* relevant local source read clear AND the managed guard is current. `residual`
|
|
92
|
+
* carries CONSTANT honest caveats (server-delivered policy, session safe-mode)
|
|
93
|
+
* that are always shown but never gate the status — otherwise `runnable` would
|
|
94
|
+
* be unreachable.
|
|
95
|
+
*/
|
|
96
|
+
export interface ExecutionReport {
|
|
97
|
+
status: "runnable" | "blocked" | "inconclusive";
|
|
98
|
+
blockers: string[];
|
|
99
|
+
unknownSources: string[];
|
|
100
|
+
residual: string[];
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Whether a `node` executable resolves on THIS process' PATH, independently of
|
|
104
|
+
* `process.versions.node`. It is a HEURISTIC proxy for the PATH Claude Code will
|
|
105
|
+
* use to spawn the exec-form handler — never proof of it.
|
|
106
|
+
*/
|
|
107
|
+
export type NodeOnPathProbe = {
|
|
108
|
+
status: "resolved";
|
|
109
|
+
version: string;
|
|
110
|
+
major: number;
|
|
111
|
+
} | {
|
|
112
|
+
status: "absent";
|
|
113
|
+
} | {
|
|
114
|
+
status: "unknown";
|
|
115
|
+
detail: string;
|
|
116
|
+
};
|
|
117
|
+
/**
|
|
118
|
+
* Resolve and run `node --version` from this process' PATH. A spawn ENOENT is
|
|
119
|
+
* `absent` (near-certainly a dead exec-form guard); a timeout, non-zero exit, or
|
|
120
|
+
* unparseable banner is honest ignorance (`unknown`) and NEVER a fabricated
|
|
121
|
+
* version.
|
|
122
|
+
*/
|
|
123
|
+
export declare function probeNodeOnPath(spawn?: SpawnFn): Promise<NodeOnPathProbe>;
|
|
124
|
+
/** Injectable seams so units never hard-read real `/etc` or `/Library`. */
|
|
125
|
+
export interface ExecutionProbeEnv {
|
|
126
|
+
platform?: NodeJS.Platform;
|
|
127
|
+
homeDir?: string;
|
|
128
|
+
env?: NodeJS.ProcessEnv;
|
|
129
|
+
/** Override the managed OS file (null → no managed file at all). */
|
|
130
|
+
managedFile?: string | null;
|
|
131
|
+
/** Override the managed drop-in dir (null → no drop-ins). */
|
|
132
|
+
managedDropInDir?: string | null;
|
|
133
|
+
/** Override the drop-in directory listing (defaults to a confined readdir). */
|
|
134
|
+
listDir?: (dir: string) => Promise<string[]>;
|
|
135
|
+
/** Injectable node-on-PATH heuristic (defaults to the real bounded probe). */
|
|
136
|
+
nodeProbe?: () => Promise<NodeOnPathProbe>;
|
|
137
|
+
}
|
|
138
|
+
/** Per-source read outcome; never promotes an unobservable source to clear. */
|
|
139
|
+
export type ExecutionSourceProbe = {
|
|
140
|
+
kind: "clear";
|
|
141
|
+
} | {
|
|
142
|
+
kind: "blocking";
|
|
143
|
+
flag: "disableAllHooks" | "allowManagedHooksOnly";
|
|
144
|
+
/** Present only for a PRESENT-but-INVALID value; names the observed shape. */
|
|
145
|
+
detail?: string;
|
|
146
|
+
} | {
|
|
147
|
+
kind: "unknown";
|
|
148
|
+
reason: string;
|
|
149
|
+
};
|
|
150
|
+
/** The already-computed component states the guard-currency check consumes. */
|
|
151
|
+
export interface ExecutionComponentStates {
|
|
152
|
+
asset: ClaudeHookComponentState;
|
|
153
|
+
settings: ClaudeHookComponentState;
|
|
154
|
+
}
|
|
155
|
+
export interface ManagedSettingsPaths {
|
|
156
|
+
file: string;
|
|
157
|
+
dropInDir: string;
|
|
158
|
+
}
|
|
159
|
+
/** Static managed-settings locations per OS (no fs). WSL reports `linux`. */
|
|
160
|
+
export declare function resolveManagedSettingsPaths(platform: NodeJS.Platform): ManagedSettingsPaths;
|
|
161
|
+
/**
|
|
162
|
+
* Probe one settings source path for the two documented hook-neutralizing
|
|
163
|
+
* flags. Fail-closed: only a genuinely-absent path or a cleanly-parsed source
|
|
164
|
+
* with no flag is `clear`; a symlink, non-regular path, permission/io error,
|
|
165
|
+
* oversized/binary content, or malformed JSON is `unknown` (unreadable ≠
|
|
166
|
+
* absent), never `clear`. `disableAllHooks` is preferred over
|
|
167
|
+
* `allowManagedHooksOnly` when both are set (the former blocks at any source).
|
|
168
|
+
*/
|
|
169
|
+
export declare function probeExecutionSource(target: string): Promise<ExecutionSourceProbe>;
|
|
170
|
+
/**
|
|
171
|
+
* Confined readdir result of the managed drop-in dir, fail-closed and mirroring
|
|
172
|
+
* `probeExecutionSource`'s errno classification: a genuinely-absent directory
|
|
173
|
+
* (ENOENT/ENOTDIR) is the ONLY empty/clear case, so it yields `{ entries: [] }`
|
|
174
|
+
* (no drop-ins). ANY other readdir failure (EACCES/EIO/ELOOP/…) means the
|
|
175
|
+
* directory is present-but-unenumerable and MUST NOT be treated as empty — it
|
|
176
|
+
* yields `{ unreadable: true }` so the caller can degrade the verdict, never
|
|
177
|
+
* silently report "no drop-ins" (a false `runnable`).
|
|
178
|
+
*/
|
|
179
|
+
export type ManagedDropInListing = {
|
|
180
|
+
unreadable?: false;
|
|
181
|
+
entries: string[];
|
|
182
|
+
} | {
|
|
183
|
+
unreadable: true;
|
|
184
|
+
reason: string;
|
|
185
|
+
};
|
|
186
|
+
export declare function listManagedDropIns(dir: string, listDir?: (dir: string) => Promise<string[]>): Promise<ManagedDropInListing>;
|
|
187
|
+
/**
|
|
188
|
+
* The fail-closed effective-execution verdict. Gathers all sources in stable
|
|
189
|
+
* order (project, local, user, managed OS file, drop-ins sorted), classifies
|
|
190
|
+
* each with `probeExecutionSource`, applies the managed-only inertness of
|
|
191
|
+
* `allowManagedHooksOnly` (blocks only from a managed source — hooks MERGE
|
|
192
|
+
* elsewhere), folds in the guard-currency blocker and the doctor-process
|
|
193
|
+
* safe-mode observation, then resolves by precedence
|
|
194
|
+
* (`blockers` first, then `unknownSources`, else `runnable`). An `unknown` is
|
|
195
|
+
* NEVER promoted to `runnable`.
|
|
196
|
+
*/
|
|
197
|
+
export declare function probeExecution(projectDir: string, componentStates: ExecutionComponentStates, env?: ExecutionProbeEnv): Promise<ExecutionReport>;
|
|
69
198
|
/**
|
|
70
199
|
* Assemble the read-only component-level doctor report (no writes). `healthy` is
|
|
71
200
|
* exactly: both components `managed-current`, matcher and command shape exact,
|
|
72
201
|
* Node `>=22`. `assetSettingsConsistent` is a reported advisory, NOT part of it.
|
|
202
|
+
* The `execution` verdict is INDEPENDENT of `healthy`.
|
|
73
203
|
*/
|
|
74
204
|
export declare function doctorClaudePreToolUse(projectDir: string, options?: {
|
|
75
205
|
manifest?: Manifest;
|
|
76
206
|
nodeVersion?: string;
|
|
207
|
+
execution?: ExecutionProbeEnv;
|
|
208
|
+
/** Injectable read-only ACL capability probe (defaults to the real one). */
|
|
209
|
+
aclProbe?: () => Promise<AclCapability>;
|
|
77
210
|
}): Promise<ClaudeHookDoctorReport>;
|
|
78
211
|
export interface ClaudeHookMutationResult {
|
|
79
212
|
ok: boolean;
|
|
@@ -81,6 +214,12 @@ export interface ClaudeHookMutationResult {
|
|
|
81
214
|
backups: string[];
|
|
82
215
|
report: ClaudeHookDoctorReport;
|
|
83
216
|
errors: string[];
|
|
217
|
+
/**
|
|
218
|
+
* NON-BLOCKING operator notices. A warning never changes `ok`: refusing to
|
|
219
|
+
* install because `node` may not resolve would leave the host with NO guard,
|
|
220
|
+
* which is strictly worse than an exec-form guard that may not resolve.
|
|
221
|
+
*/
|
|
222
|
+
warnings: string[];
|
|
84
223
|
}
|
|
85
224
|
/** Injectable deps so tests drive `_run` with a fake `PlatformSecureFs`. */
|
|
86
225
|
export interface ClaudeHookRunDeps {
|
|
@@ -89,6 +228,14 @@ export interface ClaudeHookRunDeps {
|
|
|
89
228
|
nonce?: () => string;
|
|
90
229
|
manifest?: Manifest;
|
|
91
230
|
platform?: NodeJS.Platform;
|
|
231
|
+
/**
|
|
232
|
+
* Injectable read-only ACL capability probe, forwarded to the doctor report
|
|
233
|
+
* this run embeds. Same seam as `secureFs`/`clock`: it keeps unit tests from
|
|
234
|
+
* spawning the real `getfacl` (defaults to the real probe in production).
|
|
235
|
+
*/
|
|
236
|
+
aclProbe?: () => Promise<AclCapability>;
|
|
237
|
+
/** Injectable node-on-PATH heuristic, shared by the warning and the report. */
|
|
238
|
+
nodeProbe?: () => Promise<NodeOnPathProbe>;
|
|
92
239
|
}
|
|
93
240
|
/** Internal deps-taking entry; tests drive it with a fake `PlatformSecureFs`. */
|
|
94
241
|
export declare function _run(projectDir: string, mode: "install" | "repair", options: {
|