@rafter-security/cli 0.4.2 → 0.5.3

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.
Files changed (36) hide show
  1. package/README.md +101 -1
  2. package/dist/commands/agent/audit-skill.js +6 -0
  3. package/dist/commands/agent/audit.js +15 -3
  4. package/dist/commands/agent/exec.js +9 -8
  5. package/dist/commands/agent/index.js +4 -0
  6. package/dist/commands/agent/init.js +132 -47
  7. package/dist/commands/agent/install-hook.js +2 -1
  8. package/dist/commands/agent/scan.js +180 -103
  9. package/dist/commands/agent/status.js +115 -0
  10. package/dist/commands/agent/verify.js +117 -0
  11. package/dist/commands/ci/index.js +8 -0
  12. package/dist/commands/ci/init.js +191 -0
  13. package/dist/commands/completion.js +170 -0
  14. package/dist/commands/hook/index.js +10 -0
  15. package/dist/commands/hook/posttool.js +73 -0
  16. package/dist/commands/hook/pretool.js +122 -0
  17. package/dist/commands/mcp/index.js +8 -0
  18. package/dist/commands/mcp/server.js +205 -0
  19. package/dist/commands/policy/export.js +81 -0
  20. package/dist/commands/policy/index.js +8 -0
  21. package/dist/core/audit-logger.js +2 -33
  22. package/dist/core/command-interceptor.js +6 -50
  23. package/dist/core/config-defaults.js +4 -15
  24. package/dist/core/config-manager.js +68 -0
  25. package/dist/core/custom-patterns.js +157 -0
  26. package/dist/core/policy-loader.js +167 -0
  27. package/dist/core/risk-rules.js +72 -0
  28. package/dist/index.js +26 -2
  29. package/dist/scanners/gitleaks.js +7 -6
  30. package/dist/scanners/regex-scanner.js +28 -12
  31. package/dist/utils/binary-manager.js +100 -7
  32. package/dist/utils/formatter.js +52 -0
  33. package/dist/utils/skill-manager.js +22 -9
  34. package/package.json +7 -3
  35. package/resources/pre-commit-hook.sh +45 -0
  36. package/resources/rafter-security-skill.md +323 -0
package/README.md CHANGED
@@ -73,6 +73,14 @@ rafter agent audit
73
73
  rafter agent config show
74
74
  ```
75
75
 
76
+ ## Global Options
77
+
78
+ | Flag | Description |
79
+ |------|-------------|
80
+ | `-a, --agent` | Plain output for AI agents (no colors, no emoji) |
81
+ | `-V, --version` | Print version |
82
+ | `-h, --help` | Show help |
83
+
76
84
  ## Commands
77
85
 
78
86
  ### `rafter run [options]`
@@ -169,6 +177,7 @@ Scan files or directories for secrets.
169
177
  **Options:**
170
178
  - `-q, --quiet` - Only output if secrets found
171
179
  - `--json` - Output results as JSON
180
+ - `--diff <ref>` - Scan files changed since a git ref (e.g., `HEAD~1`, `main`, `v1.0.0`)
172
181
 
173
182
  **Features:**
174
183
  - Detects 21+ secret types (AWS, GitHub, Stripe, Google, Slack, etc.)
@@ -185,6 +194,10 @@ rafter agent scan
185
194
  # Scan specific file
186
195
  rafter agent scan ./config.js
187
196
 
197
+ # Scan files changed since a ref
198
+ rafter agent scan --diff HEAD~1
199
+ rafter agent scan --diff main
200
+
188
201
  # Scan for CI (quiet mode)
189
202
  rafter agent scan --quiet
190
203
 
@@ -433,6 +446,93 @@ Claude Code skills can:
433
446
 
434
447
  Always audit skills from untrusted sources before installation. The skill-auditor provides systematic analysis to identify security risks.
435
448
 
449
+ ### `rafter mcp serve [options]`
450
+
451
+ Start an MCP server exposing Rafter security tools over stdio transport. Any MCP-compatible client (Cursor, Windsurf, Claude Desktop, Cline, etc.) can connect.
452
+
453
+ **Options:**
454
+ - `--transport <type>` - Transport type (default: `stdio`)
455
+
456
+ **MCP client config:**
457
+ ```json
458
+ {
459
+ "rafter": {
460
+ "command": "rafter",
461
+ "args": ["mcp", "serve"]
462
+ }
463
+ }
464
+ ```
465
+
466
+ **Tools provided:**
467
+
468
+ | Tool | Description |
469
+ |------|-------------|
470
+ | `scan_secrets` | Scan files/directories for hardcoded secrets. Params: `path` (required), `engine` (auto/gitleaks/patterns) |
471
+ | `evaluate_command` | Check if a shell command is allowed by Rafter policy. Params: `command` (required) |
472
+ | `read_audit_log` | Read audit log entries. Params: `limit`, `event_type`, `since` |
473
+ | `get_config` | Read Rafter config. Params: `key` (optional dot-path) |
474
+
475
+ **Resources provided:**
476
+
477
+ | URI | Description |
478
+ |-----|-------------|
479
+ | `rafter://config` | Current Rafter configuration (JSON) |
480
+ | `rafter://policy` | Active security policy — merged `.rafter.yml` + config (JSON) |
481
+
482
+ ---
483
+
484
+ ### `rafter ci init [options]`
485
+
486
+ Generate CI/CD pipeline configuration for secret scanning.
487
+
488
+ **Options:**
489
+ - `--platform <platform>` - CI platform: `github`, `gitlab`, `circleci` (default: auto-detect)
490
+ - `--output <path>` - Output file path (default: platform-specific)
491
+ - `--with-backend` - Include backend security audit job (requires `RAFTER_API_KEY`)
492
+
493
+ **Auto-detection:** Checks for `.github/`, `.gitlab-ci.yml`, `.circleci/` in the current directory.
494
+
495
+ **Examples:**
496
+ ```bash
497
+ # Auto-detect platform
498
+ rafter ci init
499
+
500
+ # Generate GitHub Actions workflow
501
+ rafter ci init --platform github
502
+
503
+ # Include backend scanning job
504
+ rafter ci init --with-backend
505
+
506
+ # Custom output path
507
+ rafter ci init --output .github/workflows/security.yml
508
+ ```
509
+
510
+ ---
511
+
512
+ ## Policy File (`.rafter.yml`)
513
+
514
+ Define per-project security policies by placing a `.rafter.yml` in your project root. The CLI walks from cwd up to git root looking for it.
515
+
516
+ ```yaml
517
+ version: "1"
518
+ risk_level: moderate
519
+ command_policy:
520
+ mode: approve-dangerous
521
+ blocked_patterns: ["rm -rf /"]
522
+ require_approval: ["npm publish"]
523
+ scan:
524
+ exclude_paths: ["vendor/", "third_party/"]
525
+ custom_patterns:
526
+ - name: "Internal API Key"
527
+ regex: "INTERNAL_[A-Z0-9]{32}"
528
+ severity: critical
529
+ audit:
530
+ retention_days: 90
531
+ log_level: info
532
+ ```
533
+
534
+ **Precedence:** Policy file values override `~/.rafter/config.json`. Arrays replace, not append.
535
+
436
536
  ---
437
537
 
438
538
  ## Configuration
@@ -466,7 +566,7 @@ Agent security settings are stored in `~/.rafter/config.json`. Key settings:
466
566
 
467
567
  **File Locations:**
468
568
  - Config: `~/.rafter/config.json`
469
- - Audit log: `~/.rafter/audit.log`
569
+ - Audit log: `~/.rafter/audit.jsonl`
470
570
  - Binaries: `~/.rafter/bin/`
471
571
  - Patterns: `~/.rafter/patterns/`
472
572
 
@@ -48,6 +48,9 @@ async function auditSkill(skillPath, opts) {
48
48
  rafterSkillInstalled
49
49
  };
50
50
  console.log(JSON.stringify(result, null, 2));
51
+ if (quickScan.secrets > 0 || quickScan.highRiskCommands.length > 0) {
52
+ process.exit(1);
53
+ }
51
54
  return;
52
55
  }
53
56
  // Check if we can use OpenClaw
@@ -85,6 +88,9 @@ async function auditSkill(skillPath, opts) {
85
88
  console.log("─".repeat(60));
86
89
  }
87
90
  console.log();
91
+ if (quickScan.secrets > 0 || quickScan.highRiskCommands.length > 0) {
92
+ process.exit(1);
93
+ }
88
94
  }
89
95
  async function runQuickScan(content) {
90
96
  // 1. Scan for secrets
@@ -1,5 +1,6 @@
1
1
  import { Command } from "commander";
2
2
  import { AuditLogger } from "../../core/audit-logger.js";
3
+ import { isAgentMode } from "../../utils/formatter.js";
3
4
  export function createAuditCommand() {
4
5
  return new Command("audit")
5
6
  .description("View audit log entries")
@@ -29,8 +30,8 @@ export function createAuditCommand() {
29
30
  console.log(`\nShowing ${entries.length} audit log entries:\n`);
30
31
  for (const entry of entries) {
31
32
  const timestamp = new Date(entry.timestamp).toLocaleString();
32
- const emoji = getEventEmoji(entry.eventType);
33
- console.log(`${emoji} [${timestamp}] ${entry.eventType}`);
33
+ const indicator = getEventIndicator(entry.eventType);
34
+ console.log(`${indicator} [${timestamp}] ${entry.eventType}`);
34
35
  if (entry.agentType) {
35
36
  console.log(` Agent: ${entry.agentType}`);
36
37
  }
@@ -52,7 +53,18 @@ export function createAuditCommand() {
52
53
  }
53
54
  });
54
55
  }
55
- function getEventEmoji(eventType) {
56
+ function getEventIndicator(eventType) {
57
+ if (isAgentMode()) {
58
+ const tagMap = {
59
+ command_intercepted: "[INTERCEPT]",
60
+ secret_detected: "[SECRET]",
61
+ content_sanitized: "[SANITIZE]",
62
+ policy_override: "[OVERRIDE]",
63
+ scan_executed: "[SCAN]",
64
+ config_changed: "[CONFIG]",
65
+ };
66
+ return tagMap[eventType] || "[EVENT]";
67
+ }
56
68
  const emojiMap = {
57
69
  command_intercepted: "🛡️",
58
70
  secret_detected: "🔑",
@@ -3,6 +3,7 @@ import { CommandInterceptor } from "../../core/command-interceptor.js";
3
3
  import { RegexScanner } from "../../scanners/regex-scanner.js";
4
4
  import { execSync } from "child_process";
5
5
  import readline from "readline";
6
+ import { fmt } from "../../utils/formatter.js";
6
7
  export function createExecCommand() {
7
8
  return new Command("exec")
8
9
  .description("Execute command with security validation")
@@ -15,7 +16,7 @@ export function createExecCommand() {
15
16
  const evaluation = interceptor.evaluate(command);
16
17
  // Step 2: Handle blocked commands
17
18
  if (!evaluation.allowed && !evaluation.requiresApproval) {
18
- console.error(`\n🚫 Command BLOCKED\n`);
19
+ console.error(`\n${fmt.error("Command BLOCKED")}\n`);
19
20
  console.error(`Risk Level: ${evaluation.riskLevel.toUpperCase()}`);
20
21
  console.error(`Reason: ${evaluation.reason}`);
21
22
  console.error(`Command: ${command}\n`);
@@ -26,7 +27,7 @@ export function createExecCommand() {
26
27
  if (!opts.skipScan && isGitCommand(command)) {
27
28
  const scanResult = await scanStagedFiles();
28
29
  if (scanResult.secretsFound) {
29
- console.error(`\n⚠️ Secrets detected in staged files!\n`);
30
+ console.error(`\n${fmt.warning("Secrets detected in staged files!")}\n`);
30
31
  console.error(`Found ${scanResult.count} secret(s) in ${scanResult.files} file(s)`);
31
32
  console.error(`\nRun 'rafter agent scan' for details.\n`);
32
33
  interceptor.logEvaluation(evaluation, "blocked");
@@ -35,7 +36,7 @@ export function createExecCommand() {
35
36
  }
36
37
  // Step 4: Handle approval required
37
38
  if (evaluation.requiresApproval && !opts.force) {
38
- console.log(`\n⚠️ Command requires approval\n`);
39
+ console.log(`\n${fmt.warning("Command requires approval")}\n`);
39
40
  console.log(`Risk Level: ${evaluation.riskLevel.toUpperCase()}`);
40
41
  console.log(`Command: ${command}`);
41
42
  if (evaluation.reason) {
@@ -44,15 +45,15 @@ export function createExecCommand() {
44
45
  console.log();
45
46
  const approved = await promptApproval();
46
47
  if (!approved) {
47
- console.log("\n❌ Command cancelled\n");
48
+ console.log(`\n${fmt.error("Command cancelled")}\n`);
48
49
  interceptor.logEvaluation(evaluation, "blocked");
49
50
  process.exit(1);
50
51
  }
51
- console.log("\n✓ Command approved by user\n");
52
+ console.log(`\n${fmt.success("Command approved by user")}\n`);
52
53
  interceptor.logEvaluation(evaluation, "overridden");
53
54
  }
54
55
  else if (opts.force && evaluation.requiresApproval) {
55
- console.log(`\n⚠️ Forcing execution (--force flag)\n`);
56
+ console.log(`\n${fmt.warning("Forcing execution (--force flag)")}\n`);
56
57
  interceptor.logEvaluation(evaluation, "overridden");
57
58
  }
58
59
  else {
@@ -64,11 +65,11 @@ export function createExecCommand() {
64
65
  stdio: "inherit",
65
66
  encoding: "utf-8"
66
67
  });
67
- console.log(`\nCommand executed successfully\n`);
68
+ console.log(`\n${fmt.success("Command executed successfully")}\n`);
68
69
  process.exit(0);
69
70
  }
70
71
  catch (e) {
71
- console.error(`\nCommand failed with exit code ${e.status}\n`);
72
+ console.error(`\n${fmt.error(`Command failed with exit code ${e.status}`)}\n`);
72
73
  process.exit(e.status || 1);
73
74
  }
74
75
  });
@@ -6,6 +6,8 @@ import { createConfigCommand } from "./config.js";
6
6
  import { createExecCommand } from "./exec.js";
7
7
  import { createAuditSkillCommand } from "./audit-skill.js";
8
8
  import { createInstallHookCommand } from "./install-hook.js";
9
+ import { createVerifyCommand } from "./verify.js";
10
+ import { createStatusCommand } from "./status.js";
9
11
  export function createAgentCommand() {
10
12
  const agent = new Command("agent")
11
13
  .description("Agent security features");
@@ -17,5 +19,7 @@ export function createAgentCommand() {
17
19
  agent.addCommand(createAuditCommand());
18
20
  agent.addCommand(createAuditSkillCommand());
19
21
  agent.addCommand(createInstallHookCommand());
22
+ agent.addCommand(createVerifyCommand());
23
+ agent.addCommand(createStatusCommand());
20
24
  return agent;
21
25
  }
@@ -6,8 +6,52 @@ import fs from "fs";
6
6
  import path from "path";
7
7
  import os from "os";
8
8
  import { fileURLToPath } from "url";
9
+ import { fmt } from "../../utils/formatter.js";
9
10
  const __filename = fileURLToPath(import.meta.url);
10
11
  const __dirname = path.dirname(__filename);
12
+ function installClaudeCodeHooks() {
13
+ const homeDir = os.homedir();
14
+ const settingsPath = path.join(homeDir, ".claude", "settings.json");
15
+ const claudeDir = path.join(homeDir, ".claude");
16
+ if (!fs.existsSync(claudeDir)) {
17
+ fs.mkdirSync(claudeDir, { recursive: true });
18
+ }
19
+ // Read existing settings or start fresh
20
+ let settings = {};
21
+ if (fs.existsSync(settingsPath)) {
22
+ try {
23
+ settings = JSON.parse(fs.readFileSync(settingsPath, "utf-8"));
24
+ }
25
+ catch {
26
+ // Corrupted file — start fresh but warn
27
+ console.log(fmt.warning("Existing settings.json was unreadable, creating new one"));
28
+ }
29
+ }
30
+ // Merge hooks — don't overwrite existing non-Rafter hooks
31
+ if (!settings.hooks)
32
+ settings.hooks = {};
33
+ if (!settings.hooks.PreToolUse)
34
+ settings.hooks.PreToolUse = [];
35
+ if (!settings.hooks.PostToolUse)
36
+ settings.hooks.PostToolUse = [];
37
+ const preHook = { type: "command", command: "rafter hook pretool" };
38
+ const postHook = { type: "command", command: "rafter hook posttool" };
39
+ // Remove any existing Rafter hooks to avoid duplicates
40
+ settings.hooks.PreToolUse = settings.hooks.PreToolUse.filter((entry) => {
41
+ const hooks = entry.hooks || [];
42
+ return !hooks.some((h) => h.command === "rafter hook pretool");
43
+ });
44
+ settings.hooks.PostToolUse = settings.hooks.PostToolUse.filter((entry) => {
45
+ const hooks = entry.hooks || [];
46
+ return !hooks.some((h) => h.command === "rafter hook posttool");
47
+ });
48
+ // Add Rafter hooks
49
+ settings.hooks.PreToolUse.push({ matcher: "Bash", hooks: [preHook] }, { matcher: "Write|Edit", hooks: [preHook] });
50
+ settings.hooks.PostToolUse.push({ matcher: ".*", hooks: [postHook] });
51
+ fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), "utf-8");
52
+ console.log(fmt.success(`Installed PreToolUse hooks to ${settingsPath}`));
53
+ console.log(fmt.success(`Installed PostToolUse hooks to ${settingsPath}`));
54
+ }
11
55
  async function installClaudeCodeSkills() {
12
56
  const homeDir = os.homedir();
13
57
  const claudeSkillsDir = path.join(homeDir, ".claude", "skills");
@@ -24,10 +68,10 @@ async function installClaudeCodeSkills() {
24
68
  }
25
69
  if (fs.existsSync(backendTemplatePath)) {
26
70
  fs.copyFileSync(backendTemplatePath, backendSkillPath);
27
- console.log(`✓ Installed Rafter Backend skill to ${backendSkillPath}`);
71
+ console.log(fmt.success(`Installed Rafter Backend skill to ${backendSkillPath}`));
28
72
  }
29
73
  else {
30
- console.log(`⚠️ Backend skill template not found at ${backendTemplatePath}`);
74
+ console.log(fmt.warning(`Backend skill template not found at ${backendTemplatePath}`));
31
75
  }
32
76
  // Install Agent Security Skill
33
77
  const agentSkillDir = path.join(claudeSkillsDir, "rafter-agent-security");
@@ -38,10 +82,10 @@ async function installClaudeCodeSkills() {
38
82
  }
39
83
  if (fs.existsSync(agentTemplatePath)) {
40
84
  fs.copyFileSync(agentTemplatePath, agentSkillPath);
41
- console.log(`✓ Installed Rafter Agent Security skill to ${agentSkillPath}`);
85
+ console.log(fmt.success(`Installed Rafter Agent Security skill to ${agentSkillPath}`));
42
86
  }
43
87
  else {
44
- console.log(`⚠️ Agent Security skill template not found at ${agentTemplatePath}`);
88
+ console.log(fmt.warning(`Agent Security skill template not found at ${agentTemplatePath}`));
45
89
  }
46
90
  }
47
91
  export function createInitCommand() {
@@ -53,100 +97,141 @@ export function createInitCommand() {
53
97
  .option("--claude-code", "Force Claude Code skill installation")
54
98
  .option("--skip-gitleaks", "Skip Gitleaks binary download")
55
99
  .action(async (opts) => {
56
- console.log("\n🛡️ Rafter Agent Security Setup");
57
- console.log("━".repeat(40));
100
+ console.log(fmt.header("Rafter Agent Security Setup"));
101
+ console.log(fmt.divider());
58
102
  console.log();
59
103
  const manager = new ConfigManager();
60
104
  // Detect environments
61
105
  const hasOpenClaw = fs.existsSync(path.join(os.homedir(), ".openclaw"));
62
106
  const hasClaudeCode = opts.claudeCode || fs.existsSync(path.join(os.homedir(), ".claude"));
63
107
  if (hasOpenClaw) {
64
- console.log("Detected environment: OpenClaw");
108
+ console.log(fmt.success("Detected environment: OpenClaw"));
65
109
  }
66
110
  else {
67
- console.log("ℹ️ OpenClaw not detected");
111
+ console.log(fmt.info("OpenClaw not detected"));
68
112
  }
69
113
  if (hasClaudeCode) {
70
- console.log("Detected environment: Claude Code");
114
+ console.log(fmt.success("Detected environment: Claude Code"));
71
115
  }
72
116
  else {
73
- console.log("ℹ️ Claude Code not detected");
117
+ console.log(fmt.info("Claude Code not detected"));
74
118
  }
75
119
  // Initialize directory structure
76
120
  try {
77
121
  await manager.initialize();
78
- console.log(`✓ Created config at ~/.rafter/config.json`);
122
+ console.log(fmt.success("Created config at ~/.rafter/config.json"));
79
123
  }
80
124
  catch (e) {
81
- console.error(`Failed to initialize: ${e}`);
125
+ console.error(fmt.error(`Failed to initialize: ${e}`));
82
126
  process.exit(1);
83
127
  }
84
128
  // Set risk level
85
129
  const validRiskLevels = ["minimal", "moderate", "aggressive"];
86
130
  if (!validRiskLevels.includes(opts.riskLevel)) {
87
- console.error(`Invalid risk level: ${opts.riskLevel}`);
131
+ console.error(fmt.error(`Invalid risk level: ${opts.riskLevel}`));
88
132
  console.error(`Valid options: ${validRiskLevels.join(", ")}`);
89
133
  process.exit(1);
90
134
  }
91
135
  manager.set("agent.riskLevel", opts.riskLevel);
92
- console.log(`✓ Set risk level: ${opts.riskLevel}`);
93
- // Download Gitleaks binary (optional)
136
+ console.log(fmt.success(`Set risk level: ${opts.riskLevel}`));
137
+ // Check / download Gitleaks binary (optional)
94
138
  if (!opts.skipGitleaks) {
95
139
  const binaryManager = new BinaryManager();
96
140
  const platformInfo = binaryManager.getPlatformInfo();
97
- if (!platformInfo.supported) {
98
- console.log(`ℹ️ Gitleaks not available for ${platformInfo.platform}/${platformInfo.arch}`);
99
- console.log("✓ Using pattern-based scanning (21 patterns)");
100
- }
101
- else if (binaryManager.isGitleaksInstalled()) {
102
- const version = await binaryManager.getGitleaksVersion();
103
- console.log(`✓ Gitleaks already installed (${version})`);
141
+ // Helper: show diagnostics for a failing binary (mirrors Python's agent init)
142
+ const showDiagnostics = async (binaryPath, verResult) => {
143
+ if (verResult.stderr) {
144
+ console.log(fmt.info(` stderr: ${verResult.stderr}`));
145
+ }
146
+ const diag = await binaryManager.collectBinaryDiagnostics(binaryPath);
147
+ if (diag) {
148
+ console.log(fmt.info("Diagnostics:"));
149
+ console.log(diag);
150
+ }
151
+ console.log(fmt.info("To fix: install gitleaks (https://github.com/gitleaks/gitleaks/releases) and ensure it is on PATH, then re-run 'rafter agent init'."));
152
+ console.log();
153
+ };
154
+ if (binaryManager.isGitleaksInstalled()) {
155
+ // Local binary exists — verify it actually works
156
+ const verResult = await binaryManager.verifyGitleaksVerbose();
157
+ if (verResult.ok) {
158
+ console.log(fmt.success(`Gitleaks already installed (${verResult.stdout})`));
159
+ }
160
+ else {
161
+ console.log(fmt.warning("Gitleaks binary found locally but failed to execute."));
162
+ console.log(fmt.info(` Binary: ${binaryManager.getGitleaksPath()}`));
163
+ await showDiagnostics(binaryManager.getGitleaksPath(), verResult);
164
+ }
104
165
  }
105
166
  else {
106
- console.log();
107
- console.log("📦 Downloading Gitleaks (enhanced secret detection)...");
108
- try {
109
- await binaryManager.downloadGitleaks((msg) => {
110
- console.log(` ${msg}`);
111
- });
112
- console.log();
167
+ // Not installed locally — check PATH (mirrors Python's shutil.which)
168
+ const pathBinary = binaryManager.findGitleaksOnPath();
169
+ if (pathBinary) {
170
+ const verResult = await binaryManager.verifyGitleaksVerbose(pathBinary);
171
+ if (verResult.ok) {
172
+ console.log(fmt.success(`Gitleaks available on PATH (${verResult.stdout})`));
173
+ }
174
+ else {
175
+ console.log(fmt.warning("Gitleaks found on PATH but failed to execute."));
176
+ console.log(fmt.info(` Binary: ${pathBinary}`));
177
+ await showDiagnostics(pathBinary, verResult);
178
+ }
113
179
  }
114
- catch (e) {
115
- console.log(`⚠️ Failed to download Gitleaks: ${e}`);
116
- console.log(" Falling back to pattern-based scanning");
180
+ else if (!platformInfo.supported) {
181
+ console.log(fmt.info(`Gitleaks not available for ${platformInfo.platform}/${platformInfo.arch}`));
182
+ console.log(fmt.success("Using pattern-based scanning (21 patterns)"));
183
+ }
184
+ else {
185
+ // Not on PATH, not installed locally — download
117
186
  console.log();
187
+ console.log(fmt.info("Downloading Gitleaks (enhanced secret detection)..."));
188
+ try {
189
+ await binaryManager.downloadGitleaks((msg) => {
190
+ console.log(` ${msg}`);
191
+ });
192
+ console.log();
193
+ }
194
+ catch (e) {
195
+ console.log();
196
+ console.log(fmt.error(`Gitleaks setup failed — pattern-based scanning will be used instead.`));
197
+ console.log(fmt.warning(String(e)));
198
+ console.log();
199
+ console.log(fmt.info("To fix: install gitleaks manually (https://github.com/gitleaks/gitleaks/releases) and ensure it is on PATH, then re-run 'rafter agent init'."));
200
+ console.log();
201
+ }
118
202
  }
119
203
  }
120
204
  }
121
205
  // Install OpenClaw skill if applicable
122
206
  if (hasOpenClaw && !opts.skipOpenclaw) {
123
- try {
124
- const skillManager = new SkillManager();
125
- const installed = await skillManager.installRafterSkill();
126
- if (installed) {
127
- console.log("✓ Installed Rafter Security skill to ~/.openclaw/skills/rafter-security.md");
128
- manager.set("agent.environments.openclaw.enabled", true);
129
- }
130
- else {
131
- console.log("⚠️ Failed to install Rafter Security skill");
132
- }
207
+ const skillManager = new SkillManager();
208
+ const result = await skillManager.installRafterSkillVerbose();
209
+ if (result.ok) {
210
+ console.log(fmt.success("Installed Rafter Security skill to ~/.openclaw/skills/rafter-security.md"));
211
+ manager.set("agent.environments.openclaw.enabled", true);
133
212
  }
134
- catch (e) {
135
- console.error(`Failed to install OpenClaw skill: ${e}`);
213
+ else {
214
+ console.log(fmt.error("Failed to install Rafter Security skill"));
215
+ console.log(fmt.warning(` Source: ${result.sourcePath}`));
216
+ console.log(fmt.warning(` Destination: ${result.destPath}`));
217
+ if (result.error) {
218
+ console.log(fmt.warning(` Error: ${result.error}`));
219
+ }
136
220
  }
137
221
  }
138
- // Install Claude Code skills if applicable
222
+ // Install Claude Code skills + hooks if applicable
139
223
  if (hasClaudeCode && !opts.skipClaudeCode) {
140
224
  try {
141
225
  await installClaudeCodeSkills();
226
+ installClaudeCodeHooks();
142
227
  manager.set("agent.environments.claudeCode.enabled", true);
143
228
  }
144
229
  catch (e) {
145
- console.error(`Failed to install Claude Code skills: ${e}`);
230
+ console.error(fmt.error(`Failed to install Claude Code integration: ${e}`));
146
231
  }
147
232
  }
148
233
  console.log();
149
- console.log("Agent security initialized!");
234
+ console.log(fmt.success("Agent security initialized!"));
150
235
  console.log();
151
236
  console.log("Next steps:");
152
237
  if (hasOpenClaw && !opts.skipOpenclaw) {
@@ -1,5 +1,6 @@
1
1
  import { Command } from "commander";
2
2
  import fs from "fs";
3
+ import os from "os";
3
4
  import path from "path";
4
5
  import { execSync } from "child_process";
5
6
  import { fileURLToPath } from 'url';
@@ -81,7 +82,7 @@ async function installLocalHook() {
81
82
  */
82
83
  async function installGlobalHook() {
83
84
  // Create global hooks directory
84
- const homeDir = process.env.HOME || process.env.USERPROFILE;
85
+ const homeDir = os.homedir();
85
86
  if (!homeDir) {
86
87
  console.error("❌ Error: Could not determine home directory");
87
88
  process.exit(1);