javi-forge 1.28.0 → 1.29.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/assets/claude-hooks/javi-forge-skillguard-pre-tool-use.mjs +828 -0
- package/assets/claude-hooks/manifest.json +1 -0
- package/dist/cli/help.d.ts +1 -1
- package/dist/cli/help.js +3 -0
- package/dist/commands/plugin.d.ts +3 -1
- package/dist/commands/plugin.js +16 -2
- package/dist/constants.d.ts +2 -0
- package/dist/constants.js +2 -0
- package/dist/lib/__fixtures__/claude-hook-ownership.d.ts +112 -0
- package/dist/lib/__fixtures__/claude-hook-ownership.js +96 -0
- package/dist/lib/agent-skills.d.ts +8 -0
- package/dist/lib/agent-skills.js +28 -61
- package/dist/lib/claude-hook-manager.d.ts +88 -0
- package/dist/lib/claude-hook-manager.js +276 -0
- package/dist/lib/claude-hook-settings.d.ts +116 -0
- package/dist/lib/claude-hook-settings.js +283 -0
- package/dist/lib/plugin.d.ts +8 -0
- package/dist/lib/plugin.js +13 -40
- package/dist/lib/skill-install-gate.d.ts +44 -8
- package/dist/lib/skill-install-gate.js +77 -8
- package/dist/lib/skill-scanner.d.ts +37 -0
- package/dist/lib/skill-scanner.js +59 -9
- package/dist/ui/AutoSkills.js +10 -0
- package/dist/ui/Plugin.js +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"schemaVersion":1,"asset":{"name":"javi-forge-skillguard-pre-tool-use.mjs","version":1,"policyVersion":1,"sha256":"78be7e6613c012280b7ad17886462ba166b63ebd031e34565d757b3a0796d7cc","historical":[]},"settingsEntries":{"current":{"version":1,"canonicalSha256":"038c59a91bf8967f6908afed74c465f1e7030254e11e4f8738975d6d708424d4"},"historical":[]},"installerHelpers":{"windowsSecureObject":null}}
|
package/dist/cli/help.d.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* Help banner shown by meow when `--help` is passed or invalid args are supplied.
|
|
9
9
|
* Multi-line template literal — preserve exact formatting (whitespace is significant).
|
|
10
10
|
*/
|
|
11
|
-
export declare const HELP_TEXT = "\n Usage\n $ javi-forge [command] [options]\n\n Commands\n init Bootstrap a new project (default)\n ci Run CI simulation (lint + compile + test + security + ghagga)\n ci validate Validate .javi-forge/ci.yaml without running anything\n ci init Install git hooks that call javi-forge ci\n tdd init Enable the TDD pre-commit section + install managed hooks\n tdd pipeline Enable the TDD pre-push section (--mode strict|warn)\n hooks run Run a git hook's composed sections (pre-commit | pre-push)\n analyze Run repoforge skills analysis\n doctor Show health report\n workflow show Render a workflow graph as ASCII (--template <name> or file path)\n workflow validate Validate project state against a workflow graph\n workflow list List available workflows and built-in templates\n plugin add Install a plugin from GitHub (org/repo)\n plugin remove Remove an installed plugin\n plugin list List installed plugins\n plugin search Search the plugin registry\n plugin validate Validate a local plugin directory\n plugin sync Auto-detect and wire installed plugins\n plugin export Export plugin to Agent Skills spec format (skills.json)\n plugin export --codex: Export plugin to Codex-compatible TOML subagent files\n plugin export-skills Generate aggregated skills.json from all installed plugins\n plugin export-skills global Generate global skills.json from all globally installed plugins\n plugin import Import an Agent Skills spec package as a javi-forge plugin\n skills doctor Show skills health report (add --deep for conflict detection)\n skills budget Show token cost of loaded skills (add -b N for custom budget)\n skills score Score a skill on quality dimensions (completeness, clarity, testability, token-efficiency)\n skills benchmark Benchmark a skill with structural quality checks\n skills auto Auto-detect project stack and suggest/install matching AI skills\n skills auto-install Alias for skills auto\n skill publish Package a skill directory for marketplace distribution (generates plugin.json)\n security baseline Create security baseline from current audit findings\n security check Check for regressions against baseline (exits non-zero if found)\n security update Re-snapshot baseline (acknowledge current vulns)\n security allowlist Add all current findings to the allowlist (suppress in future checks)\n llms-txt Generate AI-friendly llms.txt for current project\n\n Options\n --dry-run Preview changes without writing files\n --stack Project stack (node, python, go, rust, java-gradle, java-maven, elixir)\n --ci CI provider (github, gitlab, woodpecker)\n --memory Memory module (engram, obsidian-brain, memory-simple, none)\n --project-name Project name (skips name prompt)\n --ghagga Enable GHAGGA review system\n --mock Enable mock-first mode (no real API keys needed)\n --local-ai Include local AI dev stack (Ollama + Docker Compose)\n --batch Non-interactive mode (auto-proceed, no keyboard input)\n --deep Enable deep analysis (conflict + duplicate detection)\n --budget, -b Token budget limit for skills (default: 8000)\n --skills-dir Custom skills directory path\n --author Author name for skill publish\n --repo Repository URL for skill publish\n --version Show version\n --help Show this help\n\n CI options (javi-forge ci)\n --quick Lint + compile only (fast, for pre-commit)\n --shell Open interactive shell in CI container\n --detect Show detected stack and exit\n --config PATH Load ordered CI runners from a versioned config file\n (default discovery: .javi-forge/ci.yaml)\n --stack STACK Force a single explicit stack (single-stack repos only \u2014\n insufficient for hybrid repos; use --config instead)\n --no-docker Run commands natively (no Docker)\n --no-ci-ghagga Skip GHAGGA review\n --no-security Skip Semgrep security scan\n --timeout N Per-step timeout in seconds (default: 600)\n\n CI hooks (javi-forge ci init)\n Install git hooks that call javi-forge ci.\n No files copied \u2014 hooks reference the global CLI.\n Existing hooks javi-forge did not write are refused, never clobbered.\n --force Overwrite a foreign or locally modified hook. The previous\n content is copied to a .bak sibling first; if that backup\n cannot be written, the hook is left untouched. Symlinked\n hook paths are refused even with --force.\n\n SkillGuard install gate (plugin add / plugin import / skills auto)\n Every install is scanned before anything is written. Refusals are\n fail-closed and name the offending files:\n - SKILL.md files that block (critical threats) are refused \u2014 always.\n - Unscannable files (binary, oversized, unreadable) are refused unless\n --force is given.\n - Symlinks anywhere in the tree and SKILL.md files outside the declared\n set are manifest-integrity refusals \u2014 they are refused even with --force.\n - Empty or missing skills.json `skills` array on import is refused.\n\n Examples\n $ javi-forge\n $ javi-forge init --dry-run\n $ javi-forge init --stack node --ci github\n $ javi-forge ci\n $ javi-forge ci init\n $ javi-forge ci init --force\n $ javi-forge plugin add org/repo\n $ javi-forge plugin add org/repo --force\n $ javi-forge tdd init\n $ javi-forge ci --quick\n $ javi-forge ci --no-ci-ghagga --no-security\n $ javi-forge ci --no-docker\n $ javi-forge ci --shell\n $ javi-forge ci --config .javi-forge/ci.yaml\n $ javi-forge ci validate\n $ javi-forge ci --help\n $ javi-forge analyze\n $ javi-forge doctor\n $ javi-forge plugin add mapbox/agent-skills\n $ javi-forge plugin list\n";
|
|
11
|
+
export declare const HELP_TEXT = "\n Usage\n $ javi-forge [command] [options]\n\n Commands\n init Bootstrap a new project (default)\n ci Run CI simulation (lint + compile + test + security + ghagga)\n ci validate Validate .javi-forge/ci.yaml without running anything\n ci init Install git hooks that call javi-forge ci\n tdd init Enable the TDD pre-commit section + install managed hooks\n tdd pipeline Enable the TDD pre-push section (--mode strict|warn)\n hooks run Run a git hook's composed sections (pre-commit | pre-push)\n analyze Run repoforge skills analysis\n doctor Show health report\n workflow show Render a workflow graph as ASCII (--template <name> or file path)\n workflow validate Validate project state against a workflow graph\n workflow list List available workflows and built-in templates\n plugin add Install a plugin from GitHub (org/repo)\n plugin remove Remove an installed plugin\n plugin list List installed plugins\n plugin search Search the plugin registry\n plugin validate Validate a local plugin directory\n plugin sync Auto-detect and wire installed plugins\n plugin export Export plugin to Agent Skills spec format (skills.json)\n plugin export --codex: Export plugin to Codex-compatible TOML subagent files\n plugin export-skills Generate aggregated skills.json from all installed plugins\n plugin export-skills global Generate global skills.json from all globally installed plugins\n plugin import Import an Agent Skills spec package as a javi-forge plugin\n skills doctor Show skills health report (add --deep for conflict detection)\n skills budget Show token cost of loaded skills (add -b N for custom budget)\n skills score Score a skill on quality dimensions (completeness, clarity, testability, token-efficiency)\n skills benchmark Benchmark a skill with structural quality checks\n skills auto Auto-detect project stack and suggest/install matching AI skills\n skills auto-install Alias for skills auto\n skill publish Package a skill directory for marketplace distribution (generates plugin.json)\n security baseline Create security baseline from current audit findings\n security check Check for regressions against baseline (exits non-zero if found)\n security update Re-snapshot baseline (acknowledge current vulns)\n security allowlist Add all current findings to the allowlist (suppress in future checks)\n llms-txt Generate AI-friendly llms.txt for current project\n\n Options\n --dry-run Preview changes without writing files\n --stack Project stack (node, python, go, rust, java-gradle, java-maven, elixir)\n --ci CI provider (github, gitlab, woodpecker)\n --memory Memory module (engram, obsidian-brain, memory-simple, none)\n --project-name Project name (skips name prompt)\n --ghagga Enable GHAGGA review system\n --mock Enable mock-first mode (no real API keys needed)\n --local-ai Include local AI dev stack (Ollama + Docker Compose)\n --batch Non-interactive mode (auto-proceed, no keyboard input)\n --deep Enable deep analysis (conflict + duplicate detection)\n --budget, -b Token budget limit for skills (default: 8000)\n --skills-dir Custom skills directory path\n --author Author name for skill publish\n --repo Repository URL for skill publish\n --version Show version\n --help Show this help\n\n CI options (javi-forge ci)\n --quick Lint + compile only (fast, for pre-commit)\n --shell Open interactive shell in CI container\n --detect Show detected stack and exit\n --config PATH Load ordered CI runners from a versioned config file\n (default discovery: .javi-forge/ci.yaml)\n --stack STACK Force a single explicit stack (single-stack repos only \u2014\n insufficient for hybrid repos; use --config instead)\n --no-docker Run commands natively (no Docker)\n --no-ci-ghagga Skip GHAGGA review\n --no-security Skip Semgrep security scan\n --timeout N Per-step timeout in seconds (default: 600)\n\n CI hooks (javi-forge ci init)\n Install git hooks that call javi-forge ci.\n No files copied \u2014 hooks reference the global CLI.\n Existing hooks javi-forge did not write are refused, never clobbered.\n --force Overwrite a foreign or locally modified hook. The previous\n content is copied to a .bak sibling first; if that backup\n cannot be written, the hook is left untouched. Symlinked\n hook paths are refused even with --force.\n\n SkillGuard install gate (plugin add / plugin import / skills auto)\n Every install is scanned before anything is written. Refusals are\n fail-closed and name the offending files:\n - SKILL.md files that block (critical threats) are refused \u2014 always.\n - Unscannable files (binary, oversized, unreadable) are refused unless\n --force is given.\n - Symlinks anywhere in the tree and SKILL.md files outside the declared\n set are manifest-integrity refusals \u2014 they are refused even with --force.\n - Empty or missing skills.json `skills` array on import is refused.\n A refused install/auto-install exits non-zero (exit 1) so scripts and CI\n can tell a refusal apart from success; clean installs \u2014 including\n --force-lifted unscannable ones \u2014 exit 0.\n\n Examples\n $ javi-forge\n $ javi-forge init --dry-run\n $ javi-forge init --stack node --ci github\n $ javi-forge ci\n $ javi-forge ci init\n $ javi-forge ci init --force\n $ javi-forge plugin add org/repo\n $ javi-forge plugin add org/repo --force\n $ javi-forge tdd init\n $ javi-forge ci --quick\n $ javi-forge ci --no-ci-ghagga --no-security\n $ javi-forge ci --no-docker\n $ javi-forge ci --shell\n $ javi-forge ci --config .javi-forge/ci.yaml\n $ javi-forge ci validate\n $ javi-forge ci --help\n $ javi-forge analyze\n $ javi-forge doctor\n $ javi-forge plugin add mapbox/agent-skills\n $ javi-forge plugin list\n";
|
|
12
12
|
/**
|
|
13
13
|
* Per-command help for `ci`, shown by `javi-forge ci --help` (or when `ci` is
|
|
14
14
|
* given an unknown subcommand). Kept consistent with the global HELP_TEXT
|
package/dist/cli/help.js
CHANGED
|
@@ -98,6 +98,9 @@ export const HELP_TEXT = `
|
|
|
98
98
|
- Symlinks anywhere in the tree and SKILL.md files outside the declared
|
|
99
99
|
set are manifest-integrity refusals — they are refused even with --force.
|
|
100
100
|
- Empty or missing skills.json \`skills\` array on import is refused.
|
|
101
|
+
A refused install/auto-install exits non-zero (exit 1) so scripts and CI
|
|
102
|
+
can tell a refusal apart from success; clean installs — including
|
|
103
|
+
--force-lifted unscannable ones — exit 0.
|
|
101
104
|
|
|
102
105
|
Examples
|
|
103
106
|
$ javi-forge
|
|
@@ -37,7 +37,9 @@ export declare function runPluginExportCodex(name: string, onStep: StepCallback)
|
|
|
37
37
|
/**
|
|
38
38
|
* Import an Agent Skills spec package and convert to javi-forge plugin format.
|
|
39
39
|
*/
|
|
40
|
-
export declare function runPluginImport(sourceDir: string, dryRun: boolean, onStep: StepCallback,
|
|
40
|
+
export declare function runPluginImport(sourceDir: string, dryRun: boolean, onStep: StepCallback, options?: {
|
|
41
|
+
force?: boolean;
|
|
42
|
+
}): Promise<void>;
|
|
41
43
|
/**
|
|
42
44
|
* Generate a project-level skills.json from all installed plugins.
|
|
43
45
|
* Makes the project discoverable by `npx skills add` and 40+ AI agents.
|
package/dist/commands/plugin.js
CHANGED
|
@@ -18,6 +18,12 @@ export async function runPluginAdd(source, dryRun, onStep, options = {}) {
|
|
|
18
18
|
}
|
|
19
19
|
else {
|
|
20
20
|
report(onStep, stepId, `Install plugin: ${source}`, "error", result.error);
|
|
21
|
+
// FU-1 (R4-002): a skillguard refusal must be distinguishable from
|
|
22
|
+
// success by scripted consumers — exit non-zero. `process.exitCode`
|
|
23
|
+
// (not `process.exit`) so the Ink tree keeps rendering/unmounting
|
|
24
|
+
// normally. Non-gate failures (validation, clone errors) keep exit 0.
|
|
25
|
+
if (result.refused)
|
|
26
|
+
process.exitCode = 1;
|
|
21
27
|
}
|
|
22
28
|
}
|
|
23
29
|
/**
|
|
@@ -145,10 +151,13 @@ export async function runPluginExportCodex(name, onStep) {
|
|
|
145
151
|
/**
|
|
146
152
|
* Import an Agent Skills spec package and convert to javi-forge plugin format.
|
|
147
153
|
*/
|
|
148
|
-
export async function runPluginImport(sourceDir, dryRun, onStep,
|
|
154
|
+
export async function runPluginImport(sourceDir, dryRun, onStep, options = {}) {
|
|
149
155
|
const stepId = "plugin-import";
|
|
150
156
|
report(onStep, stepId, `Import agent-skills package: ${sourceDir}`, "running");
|
|
151
|
-
const result = await importAgentSkillsPackage(sourceDir, {
|
|
157
|
+
const result = await importAgentSkillsPackage(sourceDir, {
|
|
158
|
+
dryRun,
|
|
159
|
+
force: options.force,
|
|
160
|
+
});
|
|
152
161
|
if (result.success) {
|
|
153
162
|
report(onStep, stepId, `Import agent-skills package: ${sourceDir}`, "done", dryRun
|
|
154
163
|
? `dry-run: would import ${result.name}`
|
|
@@ -156,6 +165,11 @@ export async function runPluginImport(sourceDir, dryRun, onStep, force = false)
|
|
|
156
165
|
}
|
|
157
166
|
else {
|
|
158
167
|
report(onStep, stepId, `Import agent-skills package: ${sourceDir}`, "error", result.error);
|
|
168
|
+
// FU-1 (R4-002): same exit-code contract as runPluginAdd — a skillguard
|
|
169
|
+
// refusal (manifest-integrity or verdict) exits non-zero; plain input
|
|
170
|
+
// errors (skills.json missing/invalid) keep exit 0.
|
|
171
|
+
if (result.refused)
|
|
172
|
+
process.exitCode = 1;
|
|
159
173
|
}
|
|
160
174
|
}
|
|
161
175
|
/**
|
package/dist/constants.d.ts
CHANGED
|
@@ -11,6 +11,8 @@ export declare const WORKFLOWS_DIR: string;
|
|
|
11
11
|
export declare const CI_LOCAL_DIR: string;
|
|
12
12
|
/** Git hook assets directory (installed by `javi-forge ci init`) */
|
|
13
13
|
export declare const HOOK_ASSETS_DIR: string;
|
|
14
|
+
/** Standalone Claude command-hook assets directory */
|
|
15
|
+
export declare const CLAUDE_HOOK_ASSETS_DIR: string;
|
|
14
16
|
/** Security hooks template directory */
|
|
15
17
|
export declare const SECURITY_HOOKS_DIR: string;
|
|
16
18
|
/** Local AI stack template directory */
|
package/dist/constants.js
CHANGED
|
@@ -13,6 +13,8 @@ export const WORKFLOWS_DIR = path.join(FORGE_ROOT, "workflows");
|
|
|
13
13
|
export const CI_LOCAL_DIR = path.join(FORGE_ROOT, "ci-local");
|
|
14
14
|
/** Git hook assets directory (installed by `javi-forge ci init`) */
|
|
15
15
|
export const HOOK_ASSETS_DIR = path.join(FORGE_ROOT, "assets", "hooks");
|
|
16
|
+
/** Standalone Claude command-hook assets directory */
|
|
17
|
+
export const CLAUDE_HOOK_ASSETS_DIR = path.join(FORGE_ROOT, "assets", "claude-hooks");
|
|
16
18
|
/** Security hooks template directory */
|
|
17
19
|
export const SECURITY_HOOKS_DIR = path.join(TEMPLATES_DIR, "security-hooks");
|
|
18
20
|
/** Local AI stack template directory */
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared frozen ownership fixtures for the SkillGuard Claude PreToolUse guard.
|
|
3
|
+
*
|
|
4
|
+
* This module is the single source of truth for the byte/structure-exact
|
|
5
|
+
* identities the read-only recognition layer (Slice 2) classifies against:
|
|
6
|
+
*
|
|
7
|
+
* - the managed settings-entry handler group the Slice-3 writer installs;
|
|
8
|
+
* - the four v0 legacy cohort objects, copied verbatim from
|
|
9
|
+
* `templates/security-hooks/claude-settings-security.json` (never normalized);
|
|
10
|
+
* - the marker/placeholder/hash constants.
|
|
11
|
+
*
|
|
12
|
+
* Everything here is data only (frozen literals plus a couple of pure
|
|
13
|
+
* builders) so it is deterministic, host-independent, and trivially testable.
|
|
14
|
+
* The production classifier imports the constants and the legacy cohort from
|
|
15
|
+
* here; the tests additionally use the one-byte-edited and duplicate variants.
|
|
16
|
+
*/
|
|
17
|
+
/** Exact managed marker prefix on the settings handler's `statusMessage`. */
|
|
18
|
+
export declare const MANAGED_STATUS_PREFIX = "javi-forge-global-pretooluse:v1:sha256:";
|
|
19
|
+
/** Placeholder the canonical hash normalizes the live asset SHA token to. */
|
|
20
|
+
export declare const ASSET_SHA_PLACEHOLDER = "<ASSET_SHA256>";
|
|
21
|
+
/** Whole-file SHA-256 of the retained v0 legacy scaffold template. */
|
|
22
|
+
export declare const LEGACY_FILE_SHA256 = "b4638222ecddc2daac6ec3339596d853a626906bbd1233d789d80a319325c68d";
|
|
23
|
+
/** Exact managed matcher — an exact-name alternative list, never a wildcard. */
|
|
24
|
+
export declare const MANAGED_MATCHER = "Bash|PowerShell|Read|Write|Edit";
|
|
25
|
+
/** Single project-local MJS argument, kept as a literal placeholder path. */
|
|
26
|
+
export declare const MANAGED_ASSET_ARG = "${CLAUDE_PROJECT_DIR}/.claude/hooks/javi-forge-skillguard-pre-tool-use.mjs";
|
|
27
|
+
/** Exact asset filename under `.claude/hooks/`. */
|
|
28
|
+
export declare const ASSET_NAME = "javi-forge-skillguard-pre-tool-use.mjs";
|
|
29
|
+
/** Exact first-line comment marking the managed asset. */
|
|
30
|
+
export declare const ASSET_MANAGED_MARKER = "// javi-forge-managed: claude-pretooluse v1";
|
|
31
|
+
/**
|
|
32
|
+
* A representative live asset SHA. Used only to build marker `statusMessage`
|
|
33
|
+
* values in fixtures; the canonical settings identity is invariant under this
|
|
34
|
+
* value (Decision ②), which the rotation-invariance test proves.
|
|
35
|
+
*/
|
|
36
|
+
export declare const SAMPLE_ASSET_SHA256 = "78be7e6613c012280b7ad17886462ba166b63ebd031e34565d757b3a0796d7cc";
|
|
37
|
+
/** Build the exact marker `statusMessage` for a given live asset SHA. */
|
|
38
|
+
export declare function managedStatusMessage(assetSha?: string): string;
|
|
39
|
+
/** Build the exact managed command handler for a given live asset SHA. */
|
|
40
|
+
export declare function managedHandler(assetSha?: string): {
|
|
41
|
+
type: string;
|
|
42
|
+
command: string;
|
|
43
|
+
args: string[];
|
|
44
|
+
timeout: number;
|
|
45
|
+
statusMessage: string;
|
|
46
|
+
};
|
|
47
|
+
/** Build the exact managed matcher group for a given live asset SHA. */
|
|
48
|
+
export declare function managedGroup(assetSha?: string): {
|
|
49
|
+
matcher: string;
|
|
50
|
+
hooks: {
|
|
51
|
+
type: string;
|
|
52
|
+
command: string;
|
|
53
|
+
args: string[];
|
|
54
|
+
timeout: number;
|
|
55
|
+
statusMessage: string;
|
|
56
|
+
}[];
|
|
57
|
+
};
|
|
58
|
+
/** Build a valid settings container around the given hook sections. */
|
|
59
|
+
export declare function settingsContainer(pre?: unknown[], post?: unknown[], extra?: Record<string, unknown>): Record<string, unknown>;
|
|
60
|
+
export declare const L1_BASH_DANGEROUS: {
|
|
61
|
+
readonly matcher: "Bash";
|
|
62
|
+
readonly hook: "COMMAND=\"$CLAUDE_TOOL_INPUT\"\nBLOCKED_PATTERNS=(\n 'rm -rf /'\n 'rm -rf ~'\n 'chmod 777'\n 'curl.*|.*sh'\n 'wget.*|.*sh'\n 'eval.*\\$'\n 'base64.*-d.*|.*sh'\n ':(){:|:&};:'\n)\nfor pattern in \"${BLOCKED_PATTERNS[@]}\"; do\n if echo \"$COMMAND\" | grep -qE \"$pattern\"; then\n echo \"BLOCKED: Dangerous command pattern detected: $pattern\"\n exit 2\n fi\ndone\nexit 0";
|
|
63
|
+
readonly description: "Block dangerous shell commands (rm -rf /, chmod 777, pipe-to-sh, fork bombs)";
|
|
64
|
+
};
|
|
65
|
+
export declare const L2_BASH_SENSITIVE_READ: {
|
|
66
|
+
readonly matcher: "Bash";
|
|
67
|
+
readonly hook: "COMMAND=\"$CLAUDE_TOOL_INPUT\"\nSENSITIVE_PATHS=(\n '.env'\n '.env.local'\n '.env.production'\n 'credentials'\n 'secrets'\n '.ssh'\n '.gnupg'\n '.aws/credentials'\n)\nfor path in \"${SENSITIVE_PATHS[@]}\"; do\n if echo \"$COMMAND\" | grep -qE \"(cat|less|head|tail|bat|read).*$path\"; then\n echo \"BLOCKED: Attempt to read sensitive file: $path\"\n exit 2\n fi\ndone\nexit 0";
|
|
68
|
+
readonly description: "Prevent reading sensitive files (.env, credentials, SSH keys)";
|
|
69
|
+
};
|
|
70
|
+
export declare const L3_WRITE_EDIT_PROTECTED: {
|
|
71
|
+
readonly matcher: "Write|Edit";
|
|
72
|
+
readonly hook: "INPUT=\"$CLAUDE_TOOL_INPUT\"\nPROTECTED_FILES=(\n '.env'\n '.env.production'\n 'credentials.json'\n 'serviceAccountKey.json'\n '.ssh/'\n '.gnupg/'\n)\nfor pf in \"${PROTECTED_FILES[@]}\"; do\n if echo \"$INPUT\" | grep -q \"$pf\"; then\n echo \"BLOCKED: Cannot modify protected file: $pf\"\n exit 2\n fi\ndone\nexit 0";
|
|
73
|
+
readonly description: "Prevent writing to credential and secret files";
|
|
74
|
+
};
|
|
75
|
+
export declare const L4_BASH_POST_SECRET_SCAN: {
|
|
76
|
+
readonly matcher: "Bash";
|
|
77
|
+
readonly hook: "OUTPUT=\"$CLAUDE_TOOL_OUTPUT\"\nLEAK_PATTERNS=(\n 'AKIA[0-9A-Z]{16}'\n 'ghp_[A-Za-z0-9]{36}'\n 'sk_live_[0-9a-zA-Z]{24,}'\n 'xox[baprs]-[0-9a-zA-Z-]+'\n '-----BEGIN.*PRIVATE KEY-----'\n)\nfor pattern in \"${LEAK_PATTERNS[@]}\"; do\n if echo \"$OUTPUT\" | grep -qE \"$pattern\"; then\n echo \"WARNING: Command output may contain secrets. Review carefully.\"\n exit 0\n fi\ndone\nexit 0";
|
|
78
|
+
readonly description: "Warn if command output contains potential secrets";
|
|
79
|
+
};
|
|
80
|
+
/** The complete four-object v0 legacy cohort in committed order (L1–L3 Pre, L4 Post). */
|
|
81
|
+
export declare const LEGACY_COHORT: {
|
|
82
|
+
readonly L1: {
|
|
83
|
+
readonly matcher: "Bash";
|
|
84
|
+
readonly hook: "COMMAND=\"$CLAUDE_TOOL_INPUT\"\nBLOCKED_PATTERNS=(\n 'rm -rf /'\n 'rm -rf ~'\n 'chmod 777'\n 'curl.*|.*sh'\n 'wget.*|.*sh'\n 'eval.*\\$'\n 'base64.*-d.*|.*sh'\n ':(){:|:&};:'\n)\nfor pattern in \"${BLOCKED_PATTERNS[@]}\"; do\n if echo \"$COMMAND\" | grep -qE \"$pattern\"; then\n echo \"BLOCKED: Dangerous command pattern detected: $pattern\"\n exit 2\n fi\ndone\nexit 0";
|
|
85
|
+
readonly description: "Block dangerous shell commands (rm -rf /, chmod 777, pipe-to-sh, fork bombs)";
|
|
86
|
+
};
|
|
87
|
+
readonly L2: {
|
|
88
|
+
readonly matcher: "Bash";
|
|
89
|
+
readonly hook: "COMMAND=\"$CLAUDE_TOOL_INPUT\"\nSENSITIVE_PATHS=(\n '.env'\n '.env.local'\n '.env.production'\n 'credentials'\n 'secrets'\n '.ssh'\n '.gnupg'\n '.aws/credentials'\n)\nfor path in \"${SENSITIVE_PATHS[@]}\"; do\n if echo \"$COMMAND\" | grep -qE \"(cat|less|head|tail|bat|read).*$path\"; then\n echo \"BLOCKED: Attempt to read sensitive file: $path\"\n exit 2\n fi\ndone\nexit 0";
|
|
90
|
+
readonly description: "Prevent reading sensitive files (.env, credentials, SSH keys)";
|
|
91
|
+
};
|
|
92
|
+
readonly L3: {
|
|
93
|
+
readonly matcher: "Write|Edit";
|
|
94
|
+
readonly hook: "INPUT=\"$CLAUDE_TOOL_INPUT\"\nPROTECTED_FILES=(\n '.env'\n '.env.production'\n 'credentials.json'\n 'serviceAccountKey.json'\n '.ssh/'\n '.gnupg/'\n)\nfor pf in \"${PROTECTED_FILES[@]}\"; do\n if echo \"$INPUT\" | grep -q \"$pf\"; then\n echo \"BLOCKED: Cannot modify protected file: $pf\"\n exit 2\n fi\ndone\nexit 0";
|
|
95
|
+
readonly description: "Prevent writing to credential and secret files";
|
|
96
|
+
};
|
|
97
|
+
readonly L4: {
|
|
98
|
+
readonly matcher: "Bash";
|
|
99
|
+
readonly hook: "OUTPUT=\"$CLAUDE_TOOL_OUTPUT\"\nLEAK_PATTERNS=(\n 'AKIA[0-9A-Z]{16}'\n 'ghp_[A-Za-z0-9]{36}'\n 'sk_live_[0-9a-zA-Z]{24,}'\n 'xox[baprs]-[0-9a-zA-Z-]+'\n '-----BEGIN.*PRIVATE KEY-----'\n)\nfor pattern in \"${LEAK_PATTERNS[@]}\"; do\n if echo \"$OUTPUT\" | grep -qE \"$pattern\"; then\n echo \"WARNING: Command output may contain secrets. Review carefully.\"\n exit 0\n fi\ndone\nexit 0";
|
|
100
|
+
readonly description: "Warn if command output contains potential secrets";
|
|
101
|
+
};
|
|
102
|
+
};
|
|
103
|
+
/**
|
|
104
|
+
* One-byte-edited L1: a single trailing character removed from the description.
|
|
105
|
+
* It must fail deep-structural equality against the exact cohort member.
|
|
106
|
+
*/
|
|
107
|
+
export declare const L1_ONE_BYTE_EDITED: {
|
|
108
|
+
readonly description: string;
|
|
109
|
+
readonly matcher: "Bash";
|
|
110
|
+
readonly hook: "COMMAND=\"$CLAUDE_TOOL_INPUT\"\nBLOCKED_PATTERNS=(\n 'rm -rf /'\n 'rm -rf ~'\n 'chmod 777'\n 'curl.*|.*sh'\n 'wget.*|.*sh'\n 'eval.*\\$'\n 'base64.*-d.*|.*sh'\n ':(){:|:&};:'\n)\nfor pattern in \"${BLOCKED_PATTERNS[@]}\"; do\n if echo \"$COMMAND\" | grep -qE \"$pattern\"; then\n echo \"BLOCKED: Dangerous command pattern detected: $pattern\"\n exit 2\n fi\ndone\nexit 0";
|
|
111
|
+
};
|
|
112
|
+
//# sourceMappingURL=claude-hook-ownership.d.ts.map
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// biome-ignore-all lint/suspicious/noTemplateCurlyInString: verbatim shell `${…}` and the literal Claude `${CLAUDE_PROJECT_DIR}` placeholder — never JS templates.
|
|
2
|
+
/**
|
|
3
|
+
* Shared frozen ownership fixtures for the SkillGuard Claude PreToolUse guard.
|
|
4
|
+
*
|
|
5
|
+
* This module is the single source of truth for the byte/structure-exact
|
|
6
|
+
* identities the read-only recognition layer (Slice 2) classifies against:
|
|
7
|
+
*
|
|
8
|
+
* - the managed settings-entry handler group the Slice-3 writer installs;
|
|
9
|
+
* - the four v0 legacy cohort objects, copied verbatim from
|
|
10
|
+
* `templates/security-hooks/claude-settings-security.json` (never normalized);
|
|
11
|
+
* - the marker/placeholder/hash constants.
|
|
12
|
+
*
|
|
13
|
+
* Everything here is data only (frozen literals plus a couple of pure
|
|
14
|
+
* builders) so it is deterministic, host-independent, and trivially testable.
|
|
15
|
+
* The production classifier imports the constants and the legacy cohort from
|
|
16
|
+
* here; the tests additionally use the one-byte-edited and duplicate variants.
|
|
17
|
+
*/
|
|
18
|
+
/** Exact managed marker prefix on the settings handler's `statusMessage`. */
|
|
19
|
+
export const MANAGED_STATUS_PREFIX = "javi-forge-global-pretooluse:v1:sha256:";
|
|
20
|
+
/** Placeholder the canonical hash normalizes the live asset SHA token to. */
|
|
21
|
+
export const ASSET_SHA_PLACEHOLDER = "<ASSET_SHA256>";
|
|
22
|
+
/** Whole-file SHA-256 of the retained v0 legacy scaffold template. */
|
|
23
|
+
export const LEGACY_FILE_SHA256 = "b4638222ecddc2daac6ec3339596d853a626906bbd1233d789d80a319325c68d";
|
|
24
|
+
/** Exact managed matcher — an exact-name alternative list, never a wildcard. */
|
|
25
|
+
export const MANAGED_MATCHER = "Bash|PowerShell|Read|Write|Edit";
|
|
26
|
+
/** Single project-local MJS argument, kept as a literal placeholder path. */
|
|
27
|
+
export const MANAGED_ASSET_ARG = "${CLAUDE_PROJECT_DIR}/.claude/hooks/javi-forge-skillguard-pre-tool-use.mjs";
|
|
28
|
+
/** Exact asset filename under `.claude/hooks/`. */
|
|
29
|
+
export const ASSET_NAME = "javi-forge-skillguard-pre-tool-use.mjs";
|
|
30
|
+
/** Exact first-line comment marking the managed asset. */
|
|
31
|
+
export const ASSET_MANAGED_MARKER = "// javi-forge-managed: claude-pretooluse v1";
|
|
32
|
+
/**
|
|
33
|
+
* A representative live asset SHA. Used only to build marker `statusMessage`
|
|
34
|
+
* values in fixtures; the canonical settings identity is invariant under this
|
|
35
|
+
* value (Decision ②), which the rotation-invariance test proves.
|
|
36
|
+
*/
|
|
37
|
+
export const SAMPLE_ASSET_SHA256 = "78be7e6613c012280b7ad17886462ba166b63ebd031e34565d757b3a0796d7cc";
|
|
38
|
+
/** Build the exact marker `statusMessage` for a given live asset SHA. */
|
|
39
|
+
export function managedStatusMessage(assetSha = SAMPLE_ASSET_SHA256) {
|
|
40
|
+
return `${MANAGED_STATUS_PREFIX}${assetSha}`;
|
|
41
|
+
}
|
|
42
|
+
/** Build the exact managed command handler for a given live asset SHA. */
|
|
43
|
+
export function managedHandler(assetSha = SAMPLE_ASSET_SHA256) {
|
|
44
|
+
return {
|
|
45
|
+
type: "command",
|
|
46
|
+
command: "node",
|
|
47
|
+
args: [MANAGED_ASSET_ARG],
|
|
48
|
+
timeout: 30,
|
|
49
|
+
statusMessage: managedStatusMessage(assetSha),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
/** Build the exact managed matcher group for a given live asset SHA. */
|
|
53
|
+
export function managedGroup(assetSha = SAMPLE_ASSET_SHA256) {
|
|
54
|
+
return { matcher: MANAGED_MATCHER, hooks: [managedHandler(assetSha)] };
|
|
55
|
+
}
|
|
56
|
+
/** Build a valid settings container around the given hook sections. */
|
|
57
|
+
export function settingsContainer(pre = [], post = [], extra = {}) {
|
|
58
|
+
return { ...extra, hooks: { PreToolUse: pre, PostToolUse: post } };
|
|
59
|
+
}
|
|
60
|
+
// --- v0 legacy cohort (verbatim from the retained template) ------------------
|
|
61
|
+
export const L1_BASH_DANGEROUS = {
|
|
62
|
+
matcher: "Bash",
|
|
63
|
+
hook: "COMMAND=\"$CLAUDE_TOOL_INPUT\"\nBLOCKED_PATTERNS=(\n 'rm -rf /'\n 'rm -rf ~'\n 'chmod 777'\n 'curl.*|.*sh'\n 'wget.*|.*sh'\n 'eval.*\\$'\n 'base64.*-d.*|.*sh'\n ':(){:|:&};:'\n)\nfor pattern in \"${BLOCKED_PATTERNS[@]}\"; do\n if echo \"$COMMAND\" | grep -qE \"$pattern\"; then\n echo \"BLOCKED: Dangerous command pattern detected: $pattern\"\n exit 2\n fi\ndone\nexit 0",
|
|
64
|
+
description: "Block dangerous shell commands (rm -rf /, chmod 777, pipe-to-sh, fork bombs)",
|
|
65
|
+
};
|
|
66
|
+
export const L2_BASH_SENSITIVE_READ = {
|
|
67
|
+
matcher: "Bash",
|
|
68
|
+
hook: "COMMAND=\"$CLAUDE_TOOL_INPUT\"\nSENSITIVE_PATHS=(\n '.env'\n '.env.local'\n '.env.production'\n 'credentials'\n 'secrets'\n '.ssh'\n '.gnupg'\n '.aws/credentials'\n)\nfor path in \"${SENSITIVE_PATHS[@]}\"; do\n if echo \"$COMMAND\" | grep -qE \"(cat|less|head|tail|bat|read).*$path\"; then\n echo \"BLOCKED: Attempt to read sensitive file: $path\"\n exit 2\n fi\ndone\nexit 0",
|
|
69
|
+
description: "Prevent reading sensitive files (.env, credentials, SSH keys)",
|
|
70
|
+
};
|
|
71
|
+
export const L3_WRITE_EDIT_PROTECTED = {
|
|
72
|
+
matcher: "Write|Edit",
|
|
73
|
+
hook: "INPUT=\"$CLAUDE_TOOL_INPUT\"\nPROTECTED_FILES=(\n '.env'\n '.env.production'\n 'credentials.json'\n 'serviceAccountKey.json'\n '.ssh/'\n '.gnupg/'\n)\nfor pf in \"${PROTECTED_FILES[@]}\"; do\n if echo \"$INPUT\" | grep -q \"$pf\"; then\n echo \"BLOCKED: Cannot modify protected file: $pf\"\n exit 2\n fi\ndone\nexit 0",
|
|
74
|
+
description: "Prevent writing to credential and secret files",
|
|
75
|
+
};
|
|
76
|
+
export const L4_BASH_POST_SECRET_SCAN = {
|
|
77
|
+
matcher: "Bash",
|
|
78
|
+
hook: "OUTPUT=\"$CLAUDE_TOOL_OUTPUT\"\nLEAK_PATTERNS=(\n 'AKIA[0-9A-Z]{16}'\n 'ghp_[A-Za-z0-9]{36}'\n 'sk_live_[0-9a-zA-Z]{24,}'\n 'xox[baprs]-[0-9a-zA-Z-]+'\n '-----BEGIN.*PRIVATE KEY-----'\n)\nfor pattern in \"${LEAK_PATTERNS[@]}\"; do\n if echo \"$OUTPUT\" | grep -qE \"$pattern\"; then\n echo \"WARNING: Command output may contain secrets. Review carefully.\"\n exit 0\n fi\ndone\nexit 0",
|
|
79
|
+
description: "Warn if command output contains potential secrets",
|
|
80
|
+
};
|
|
81
|
+
/** The complete four-object v0 legacy cohort in committed order (L1–L3 Pre, L4 Post). */
|
|
82
|
+
export const LEGACY_COHORT = {
|
|
83
|
+
L1: L1_BASH_DANGEROUS,
|
|
84
|
+
L2: L2_BASH_SENSITIVE_READ,
|
|
85
|
+
L3: L3_WRITE_EDIT_PROTECTED,
|
|
86
|
+
L4: L4_BASH_POST_SECRET_SCAN,
|
|
87
|
+
};
|
|
88
|
+
/**
|
|
89
|
+
* One-byte-edited L1: a single trailing character removed from the description.
|
|
90
|
+
* It must fail deep-structural equality against the exact cohort member.
|
|
91
|
+
*/
|
|
92
|
+
export const L1_ONE_BYTE_EDITED = {
|
|
93
|
+
...L1_BASH_DANGEROUS,
|
|
94
|
+
description: "Block dangerous shell commands (rm -rf /, chmod 777, pipe-to-sh, fork bombs)".slice(0, -1),
|
|
95
|
+
};
|
|
96
|
+
//# sourceMappingURL=claude-hook-ownership.js.map
|
|
@@ -35,6 +35,14 @@ export declare function importAgentSkillsPackage(sourceDir: string, options?: {
|
|
|
35
35
|
success: boolean;
|
|
36
36
|
name?: string;
|
|
37
37
|
error?: string;
|
|
38
|
+
/**
|
|
39
|
+
* FU-1 (R4-002): true when the failure is a skillguard gate refusal
|
|
40
|
+
* (manifest-integrity — invalid name, empty/missing skills, containment
|
|
41
|
+
* escape, walk errors/symlinks/undeclared — or verdict refusal, incl. a
|
|
42
|
+
* scanner-error deny). The CLI layer turns this into a non-zero exit
|
|
43
|
+
* code. Plain input errors (skills.json missing/invalid) leave it unset.
|
|
44
|
+
*/
|
|
45
|
+
refused?: boolean;
|
|
38
46
|
}>;
|
|
39
47
|
/**
|
|
40
48
|
* Aggregate multiple installed plugins into a single Agent Skills spec manifest.
|
package/dist/lib/agent-skills.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import fs from "fs-extra";
|
|
3
3
|
import { AGENT_SKILLS_MANIFEST_FILE, PLUGIN_MANIFEST_FILE, PLUGINS_DIR, } from "../constants.js";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
4
|
+
import { evaluateCoverageGate, scanFailureMessage, } from "./skill-install-gate.js";
|
|
5
|
+
import { checkPathContained, scanSkillsWithCoverage } from "./skill-scanner.js";
|
|
6
6
|
// ── Conversion ─────────────────────────────────────────────────────────────
|
|
7
7
|
/**
|
|
8
8
|
* Convert a javi-forge PluginManifest to an Agent Skills spec manifest.
|
|
@@ -111,6 +111,7 @@ export async function importAgentSkillsPackage(sourceDir, options = {}) {
|
|
|
111
111
|
if (typeof pluginName !== "string") {
|
|
112
112
|
return {
|
|
113
113
|
success: false,
|
|
114
|
+
refused: true,
|
|
114
115
|
error: `skillguard: install refused — invalid manifest name "${pluginName}" (manifest-integrity, force never lifts)`,
|
|
115
116
|
};
|
|
116
117
|
}
|
|
@@ -122,6 +123,7 @@ export async function importAgentSkillsPackage(sourceDir, options = {}) {
|
|
|
122
123
|
path.isAbsolute(pluginName)) {
|
|
123
124
|
return {
|
|
124
125
|
success: false,
|
|
126
|
+
refused: true,
|
|
125
127
|
error: `skillguard: install refused — invalid manifest name "${pluginName}" (manifest-integrity, force never lifts)`,
|
|
126
128
|
};
|
|
127
129
|
}
|
|
@@ -133,6 +135,7 @@ export async function importAgentSkillsPackage(sourceDir, options = {}) {
|
|
|
133
135
|
agentManifest.skills.length === 0) {
|
|
134
136
|
return {
|
|
135
137
|
success: false,
|
|
138
|
+
refused: true,
|
|
136
139
|
error: "skills.json must declare a non-empty skills array (every skill-shaped file must be declared)",
|
|
137
140
|
};
|
|
138
141
|
}
|
|
@@ -143,12 +146,14 @@ export async function importAgentSkillsPackage(sourceDir, options = {}) {
|
|
|
143
146
|
if (!entry || typeof entry.name !== "string" || !entry.name) {
|
|
144
147
|
return {
|
|
145
148
|
success: false,
|
|
149
|
+
refused: true,
|
|
146
150
|
error: "skills.json skills entry missing name",
|
|
147
151
|
};
|
|
148
152
|
}
|
|
149
153
|
if (typeof entry.path !== "string" || !entry.path) {
|
|
150
154
|
return {
|
|
151
155
|
success: false,
|
|
156
|
+
refused: true,
|
|
152
157
|
error: `skills.json skills entry "${entry.name}" missing path`,
|
|
153
158
|
};
|
|
154
159
|
}
|
|
@@ -156,6 +161,7 @@ export async function importAgentSkillsPackage(sourceDir, options = {}) {
|
|
|
156
161
|
if (!contained.ok) {
|
|
157
162
|
return {
|
|
158
163
|
success: false,
|
|
164
|
+
refused: true,
|
|
159
165
|
error: `skills.json skills entry "${entry.name}" path escapes package root (${contained.reason}) — refusing`,
|
|
160
166
|
};
|
|
161
167
|
}
|
|
@@ -169,49 +175,25 @@ export async function importAgentSkillsPackage(sourceDir, options = {}) {
|
|
|
169
175
|
// Runs BEFORE the existing-install remove and fs.copy: a refusal preserves
|
|
170
176
|
// an existing install and installs nothing. dryRun early-returns above, so
|
|
171
177
|
// no scan happens on dry-run. Scanner/eval errors deny unconditionally (D7).
|
|
178
|
+
// The refusal policy + message-building is shared with plugin add via
|
|
179
|
+
// evaluateCoverageGate (R2-001).
|
|
180
|
+
let coverage;
|
|
172
181
|
try {
|
|
173
|
-
|
|
174
|
-
// Manifest-integrity refusals — block-level, force NEVER lifts
|
|
175
|
-
// (JD-007: ANY symlink; JD-006: undeclared SKILL.md incl. node_modules).
|
|
176
|
-
// A walk with I/O errors cannot certify the copied footprint — refuse
|
|
177
|
-
// first, because the broken subtree may hide symlinks or undeclared
|
|
178
|
-
// files (JD-013).
|
|
179
|
-
if (coverage.errors.length > 0) {
|
|
180
|
-
return {
|
|
181
|
-
success: false,
|
|
182
|
-
error: `skillguard: install refused — ${coverage.errors.length} path(s) could not be read (walk incomplete; manifest-integrity, force never lifts):\n${coverage.errors.map((p) => ` ${p}`).join("\n")}`,
|
|
183
|
-
};
|
|
184
|
-
}
|
|
185
|
-
if (coverage.symlinks.length > 0) {
|
|
186
|
-
return {
|
|
187
|
-
success: false,
|
|
188
|
-
error: `skillguard: install refused — symlink(s) in tree (manifest-integrity, force never lifts):\n${coverage.symlinks.map((p) => ` ${p}`).join("\n")}`,
|
|
189
|
-
};
|
|
190
|
-
}
|
|
191
|
-
if (coverage.undeclared.length > 0) {
|
|
192
|
-
return {
|
|
193
|
-
success: false,
|
|
194
|
-
error: `skillguard: install refused — undeclared SKILL.md(s) in tree (every skill-shaped file must be declared; force never lifts):\n${coverage.undeclared.map((p) => ` ${p}`).join("\n")}`,
|
|
195
|
-
};
|
|
196
|
-
}
|
|
197
|
-
const gate = evaluateInstallGate(coverage.declared, { force });
|
|
198
|
-
if (!gate.allowed) {
|
|
199
|
-
const blocked = gate.rejected.filter((r) => r.verdict === "block").length;
|
|
200
|
-
const unscannable = gate.rejected.filter((r) => r.verdict === "unscannable").length;
|
|
201
|
-
return {
|
|
202
|
-
success: false,
|
|
203
|
-
// Lead line names the rejected count; the batch report renders
|
|
204
|
-
// the FULL declared set so the header/rows reflect every scanned
|
|
205
|
-
// skill (D6, JD-014).
|
|
206
|
-
error: `skillguard: install refused — ${gate.rejected.length} rejected (${blocked} blocked, ${unscannable} unscannable)\n${formatBatchReport(coverage.declared)}`,
|
|
207
|
-
};
|
|
208
|
-
}
|
|
182
|
+
coverage = await scanSkillsWithCoverage(sourceDir, declaredPaths);
|
|
209
183
|
}
|
|
210
184
|
catch (scanError) {
|
|
211
|
-
const msg = scanError instanceof Error ? scanError.message : String(scanError);
|
|
212
185
|
return {
|
|
213
186
|
success: false,
|
|
214
|
-
|
|
187
|
+
refused: true,
|
|
188
|
+
error: scanFailureMessage(scanError),
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
const decision = evaluateCoverageGate(coverage, { force });
|
|
192
|
+
if (decision.refusalError) {
|
|
193
|
+
return {
|
|
194
|
+
success: false,
|
|
195
|
+
refused: true,
|
|
196
|
+
error: decision.refusalError,
|
|
215
197
|
};
|
|
216
198
|
}
|
|
217
199
|
// Remove existing version if present
|
|
@@ -242,33 +224,18 @@ export async function importAgentSkillsPackage(sourceDir, options = {}) {
|
|
|
242
224
|
* Verify a declared skill entry path stays inside the package root — both
|
|
243
225
|
* lexically (`../../x`, absolute paths) and by realpath, so an in-tree symlink
|
|
244
226
|
* cannot redirect the import read outside the staged clone (JD-003/JD-006).
|
|
245
|
-
*
|
|
246
|
-
*
|
|
247
|
-
*
|
|
227
|
+
* Non-throwing `{ ok, reason }` surface over the shared containment core
|
|
228
|
+
* (`checkPathContained`, R2-002) — the import gate refuses gracefully instead
|
|
229
|
+
* of throwing.
|
|
248
230
|
*/
|
|
249
231
|
async function skillPathContained(rootAbs, rootReal, entryPath) {
|
|
250
|
-
const
|
|
251
|
-
|
|
252
|
-
if (rel.startsWith("..") || path.isAbsolute(rel)) {
|
|
232
|
+
const check = await checkPathContained(rootAbs, rootReal, entryPath);
|
|
233
|
+
if (!check.ok) {
|
|
253
234
|
return {
|
|
254
235
|
ok: false,
|
|
255
|
-
reason: `path "${entryPath}" resolves outside the package root`,
|
|
236
|
+
reason: `path "${entryPath}" resolves outside the package root${check.violation === "realpath" ? " (realpath)" : ""}`,
|
|
256
237
|
};
|
|
257
238
|
}
|
|
258
|
-
try {
|
|
259
|
-
const entryReal = await fs.realpath(entryAbs);
|
|
260
|
-
const relReal = path.relative(rootReal, entryReal);
|
|
261
|
-
if (relReal.startsWith("..") || path.isAbsolute(relReal)) {
|
|
262
|
-
return {
|
|
263
|
-
ok: false,
|
|
264
|
-
reason: `path "${entryPath}" resolves outside the package root (realpath)`,
|
|
265
|
-
};
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
catch {
|
|
269
|
-
// Declared dir does not exist yet — lexical containment stands; the
|
|
270
|
-
// coverage walk reports it as a missing declared skill later.
|
|
271
|
-
}
|
|
272
239
|
return { ok: true };
|
|
273
240
|
}
|
|
274
241
|
// ── Aggregation ──────────────────────────────────────────────────────────
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read-only Claude PreToolUse ownership manager (Slice 2). Its only filesystem
|
|
3
|
+
* surface is `safeReadFile` plus one isolated no-follow `lstat` helper — it never
|
|
4
|
+
* writes, creates directories, or makes backups. It owns asset byte
|
|
5
|
+
* classification (always recompute the full-file SHA), the settings read+parse
|
|
6
|
+
* wrapper (identity delegated to `claude-hook-settings`), the Node `>=22` check,
|
|
7
|
+
* and the component-level doctor. Install/repair are declared but unimplemented
|
|
8
|
+
* Slice-3 seams — Slice 3 GROWS this file, it does not relocate this code.
|
|
9
|
+
*/
|
|
10
|
+
import { type ClaudeHookComponentState, type SettingsClassification, type SettingsIdentityManifest } from "./claude-hook-settings.js";
|
|
11
|
+
declare const COVERAGE: readonly ["Bash", "PowerShell", "Read", "Write", "Edit"];
|
|
12
|
+
export interface AssetManifestEntry {
|
|
13
|
+
name: string;
|
|
14
|
+
version: number;
|
|
15
|
+
sha256: string;
|
|
16
|
+
historical: string[];
|
|
17
|
+
}
|
|
18
|
+
export interface Manifest {
|
|
19
|
+
asset: AssetManifestEntry;
|
|
20
|
+
settingsEntries: SettingsIdentityManifest;
|
|
21
|
+
}
|
|
22
|
+
export interface ClaudeHookAssetClassification {
|
|
23
|
+
state: ClaudeHookComponentState;
|
|
24
|
+
version?: number;
|
|
25
|
+
sha256?: string;
|
|
26
|
+
detail?: string;
|
|
27
|
+
}
|
|
28
|
+
export interface ClaudeHookDoctorReport {
|
|
29
|
+
healthy: boolean;
|
|
30
|
+
settings: {
|
|
31
|
+
state: ClaudeHookComponentState;
|
|
32
|
+
version?: number;
|
|
33
|
+
canonicalSha256?: string;
|
|
34
|
+
detail: string;
|
|
35
|
+
};
|
|
36
|
+
asset: {
|
|
37
|
+
state: ClaudeHookComponentState;
|
|
38
|
+
version?: number;
|
|
39
|
+
sha256?: string;
|
|
40
|
+
detail: string;
|
|
41
|
+
};
|
|
42
|
+
node: {
|
|
43
|
+
available: boolean;
|
|
44
|
+
version?: string;
|
|
45
|
+
satisfiesMinimum: boolean;
|
|
46
|
+
};
|
|
47
|
+
matcherExact: boolean;
|
|
48
|
+
commandShapeExact: boolean;
|
|
49
|
+
assetSettingsConsistent: boolean;
|
|
50
|
+
coverage: typeof COVERAGE;
|
|
51
|
+
hostResidual: string;
|
|
52
|
+
remediation: readonly string[];
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Classify the asset into one of nine states from observed bytes only. Never
|
|
56
|
+
* trusts a claimed hash: the full-file SHA is always recomputed and compared to
|
|
57
|
+
* the manifest.
|
|
58
|
+
*/
|
|
59
|
+
export declare function classifyAssetState(assetPath: string, manifest: Manifest): Promise<ClaudeHookAssetClassification>;
|
|
60
|
+
/** Read `.claude/settings.json` and classify it (lstat → bounded read → legacy SHA → pure classifier). */
|
|
61
|
+
export declare function classifySettingsFile(settingsPath: string, currentAssetSha: string, identities: SettingsIdentityManifest): Promise<SettingsClassification>;
|
|
62
|
+
/** Node availability + `>=22` check from a version string (no spawn). */
|
|
63
|
+
export declare function detectNode(nodeVersion: string | undefined): {
|
|
64
|
+
available: boolean;
|
|
65
|
+
version?: string;
|
|
66
|
+
satisfiesMinimum: boolean;
|
|
67
|
+
};
|
|
68
|
+
/**
|
|
69
|
+
* Assemble the read-only component-level doctor report (no writes). `healthy` is
|
|
70
|
+
* exactly: both components `managed-current`, matcher and command shape exact,
|
|
71
|
+
* Node `>=22`. `assetSettingsConsistent` is a reported advisory, NOT part of it.
|
|
72
|
+
*/
|
|
73
|
+
export declare function doctorClaudePreToolUse(projectDir: string, options?: {
|
|
74
|
+
manifest?: Manifest;
|
|
75
|
+
nodeVersion?: string;
|
|
76
|
+
}): Promise<ClaudeHookDoctorReport>;
|
|
77
|
+
export interface ClaudeHookMutationResult {
|
|
78
|
+
ok: boolean;
|
|
79
|
+
changed: string[];
|
|
80
|
+
backups: string[];
|
|
81
|
+
errors: string[];
|
|
82
|
+
}
|
|
83
|
+
export declare function installClaudePreToolUse(_projectDir: string): Promise<ClaudeHookMutationResult>;
|
|
84
|
+
export declare function repairClaudePreToolUse(_projectDir: string, _options?: {
|
|
85
|
+
force?: boolean;
|
|
86
|
+
}): Promise<ClaudeHookMutationResult>;
|
|
87
|
+
export {};
|
|
88
|
+
//# sourceMappingURL=claude-hook-manager.d.ts.map
|