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.
@@ -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}}
@@ -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, force?: boolean): Promise<void>;
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.
@@ -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, force = false) {
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, { dryRun, force });
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
  /**
@@ -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.
@@ -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 { evaluateInstallGate } from "./skill-install-gate.js";
5
- import { formatBatchReport, scanSkillsWithCoverage } from "./skill-scanner.js";
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
- const coverage = await scanSkillsWithCoverage(sourceDir, declaredPaths);
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
- error: `skillguard scan failed — ${msg}`,
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
- * Realpath resolution is best-effort: a missing declared dir has no realpath
246
- * yet, in which case lexical containment is the whole guard (the coverage walk
247
- * will later report it as a missing/unscannable declared skill).
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 entryAbs = path.resolve(rootAbs, entryPath);
251
- const rel = path.relative(rootAbs, entryAbs);
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