@rafter-security/cli 0.6.5 → 0.7.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.
@@ -8,9 +8,45 @@ import os from "os";
8
8
  import { execSync } from "child_process";
9
9
  import { fileURLToPath } from "url";
10
10
  import { createRequire } from "module";
11
+ import { createInterface } from "readline";
11
12
  import { fmt } from "../../utils/formatter.js";
13
+ import { injectInstructionFile } from "./instruction-block.js";
12
14
  const __filename = fileURLToPath(import.meta.url);
13
15
  const __dirname = path.dirname(__filename);
16
+ /**
17
+ * Install global instruction files for platforms that support them.
18
+ *
19
+ * Only Claude Code (~/.claude/CLAUDE.md) and Cursor (~/.cursor/rules/*.mdc)
20
+ * have confirmed global instruction file paths. Other platforms (Codex, Gemini,
21
+ * Windsurf, Continue.dev, Aider) only support project-level instruction files
22
+ * (AGENTS.md, GEMINI.md, .windsurfrules, .continuerules, .aider/conventions.md)
23
+ * which are handled by `rafter agent init-project` (not yet implemented).
24
+ */
25
+ function installGlobalInstructions(platforms) {
26
+ const homeDir = os.homedir();
27
+ // Claude Code — ~/.claude/CLAUDE.md (confirmed global instruction file)
28
+ if (platforms.claudeCode) {
29
+ try {
30
+ const filePath = path.join(homeDir, ".claude", "CLAUDE.md");
31
+ injectInstructionFile(filePath);
32
+ console.log(fmt.success(`Installed Rafter instructions to ${filePath}`));
33
+ }
34
+ catch (e) {
35
+ console.log(fmt.warning(`Failed to write Claude Code instructions: ${e}`));
36
+ }
37
+ }
38
+ // Cursor — ~/.cursor/rules/rafter-security.mdc (global rules directory, markdown format)
39
+ if (platforms.cursor) {
40
+ try {
41
+ const filePath = path.join(homeDir, ".cursor", "rules", "rafter-security.mdc");
42
+ injectInstructionFile(filePath);
43
+ console.log(fmt.success(`Installed Rafter instructions to ${filePath}`));
44
+ }
45
+ catch (e) {
46
+ console.log(fmt.warning(`Failed to write Cursor instructions: ${e}`));
47
+ }
48
+ }
49
+ }
14
50
  function installClaudeCodeHooks() {
15
51
  const homeDir = os.homedir();
16
52
  const settingsPath = path.join(homeDir, ".claude", "settings.json");
@@ -54,6 +90,175 @@ function installClaudeCodeHooks() {
54
90
  console.log(fmt.success(`Installed PreToolUse hooks to ${settingsPath}`));
55
91
  console.log(fmt.success(`Installed PostToolUse hooks to ${settingsPath}`));
56
92
  }
93
+ function installCodexHooks() {
94
+ const homeDir = os.homedir();
95
+ const codexDir = path.join(homeDir, ".codex");
96
+ if (!fs.existsSync(codexDir)) {
97
+ fs.mkdirSync(codexDir, { recursive: true });
98
+ }
99
+ const hooksPath = path.join(codexDir, "hooks.json");
100
+ let config = {};
101
+ if (fs.existsSync(hooksPath)) {
102
+ try {
103
+ config = JSON.parse(fs.readFileSync(hooksPath, "utf-8"));
104
+ }
105
+ catch {
106
+ console.log(fmt.warning("Existing Codex hooks.json was unreadable, creating new one"));
107
+ }
108
+ }
109
+ if (!config.hooks)
110
+ config.hooks = {};
111
+ if (!config.hooks.PreToolUse)
112
+ config.hooks.PreToolUse = [];
113
+ if (!config.hooks.PostToolUse)
114
+ config.hooks.PostToolUse = [];
115
+ // Codex uses the same hookSpecificOutput protocol as Claude Code (format=claude)
116
+ const preHook = { type: "command", command: "rafter hook pretool" };
117
+ const postHook = { type: "command", command: "rafter hook posttool" };
118
+ // Remove existing rafter hooks
119
+ config.hooks.PreToolUse = config.hooks.PreToolUse.filter((entry) => !(entry.hooks || []).some((h) => h.command?.startsWith("rafter hook pretool")));
120
+ config.hooks.PostToolUse = config.hooks.PostToolUse.filter((entry) => !(entry.hooks || []).some((h) => h.command?.startsWith("rafter hook posttool")));
121
+ config.hooks.PreToolUse.push({ matcher: "Bash", hooks: [preHook] });
122
+ config.hooks.PostToolUse.push({ matcher: ".*", hooks: [postHook] });
123
+ fs.writeFileSync(hooksPath, JSON.stringify(config, null, 2), "utf-8");
124
+ console.log(fmt.success(`Installed hooks to ${hooksPath}`));
125
+ }
126
+ function installCursorHooks() {
127
+ const homeDir = os.homedir();
128
+ const cursorDir = path.join(homeDir, ".cursor");
129
+ if (!fs.existsSync(cursorDir)) {
130
+ fs.mkdirSync(cursorDir, { recursive: true });
131
+ }
132
+ const hooksPath = path.join(cursorDir, "hooks.json");
133
+ let config = {};
134
+ if (fs.existsSync(hooksPath)) {
135
+ try {
136
+ config = JSON.parse(fs.readFileSync(hooksPath, "utf-8"));
137
+ }
138
+ catch {
139
+ console.log(fmt.warning("Existing Cursor hooks.json was unreadable, creating new one"));
140
+ }
141
+ }
142
+ if (!config.version)
143
+ config.version = 1;
144
+ if (!config.hooks)
145
+ config.hooks = {};
146
+ if (!config.hooks.beforeShellExecution)
147
+ config.hooks.beforeShellExecution = [];
148
+ // Remove existing rafter hooks
149
+ config.hooks.beforeShellExecution = config.hooks.beforeShellExecution.filter((entry) => !entry.command?.includes("rafter hook pretool"));
150
+ config.hooks.beforeShellExecution.push({
151
+ command: "rafter hook pretool --format cursor",
152
+ type: "command",
153
+ timeout: 5000,
154
+ });
155
+ fs.writeFileSync(hooksPath, JSON.stringify(config, null, 2), "utf-8");
156
+ console.log(fmt.success(`Installed hooks to ${hooksPath}`));
157
+ }
158
+ function installGeminiHooks() {
159
+ const homeDir = os.homedir();
160
+ const geminiDir = path.join(homeDir, ".gemini");
161
+ if (!fs.existsSync(geminiDir)) {
162
+ fs.mkdirSync(geminiDir, { recursive: true });
163
+ }
164
+ const settingsPath = path.join(geminiDir, "settings.json");
165
+ let settings = {};
166
+ if (fs.existsSync(settingsPath)) {
167
+ try {
168
+ settings = JSON.parse(fs.readFileSync(settingsPath, "utf-8"));
169
+ }
170
+ catch {
171
+ console.log(fmt.warning("Existing Gemini settings.json was unreadable, creating new one"));
172
+ }
173
+ }
174
+ if (!settings.hooks)
175
+ settings.hooks = {};
176
+ if (!settings.hooks.BeforeTool)
177
+ settings.hooks.BeforeTool = [];
178
+ if (!settings.hooks.AfterTool)
179
+ settings.hooks.AfterTool = [];
180
+ // Remove existing rafter hooks
181
+ settings.hooks.BeforeTool = settings.hooks.BeforeTool.filter((entry) => !(entry.hooks || []).some((h) => h.command?.includes("rafter hook pretool")));
182
+ settings.hooks.AfterTool = settings.hooks.AfterTool.filter((entry) => !(entry.hooks || []).some((h) => h.command?.includes("rafter hook posttool")));
183
+ settings.hooks.BeforeTool.push({
184
+ matcher: "shell|write_file",
185
+ hooks: [{ type: "command", command: "rafter hook pretool --format gemini", timeout: 5000 }],
186
+ });
187
+ settings.hooks.AfterTool.push({
188
+ matcher: ".*",
189
+ hooks: [{ type: "command", command: "rafter hook posttool --format gemini", timeout: 5000 }],
190
+ });
191
+ fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), "utf-8");
192
+ console.log(fmt.success(`Installed hooks to ${settingsPath}`));
193
+ }
194
+ function installWindsurfHooks() {
195
+ const homeDir = os.homedir();
196
+ const windsurfDir = path.join(homeDir, ".windsurf");
197
+ if (!fs.existsSync(windsurfDir)) {
198
+ fs.mkdirSync(windsurfDir, { recursive: true });
199
+ }
200
+ const hooksPath = path.join(windsurfDir, "hooks.json");
201
+ let config = {};
202
+ if (fs.existsSync(hooksPath)) {
203
+ try {
204
+ config = JSON.parse(fs.readFileSync(hooksPath, "utf-8"));
205
+ }
206
+ catch {
207
+ console.log(fmt.warning("Existing Windsurf hooks.json was unreadable, creating new one"));
208
+ }
209
+ }
210
+ if (!config.hooks)
211
+ config.hooks = {};
212
+ if (!config.hooks.pre_run_command)
213
+ config.hooks.pre_run_command = [];
214
+ if (!config.hooks.pre_write_code)
215
+ config.hooks.pre_write_code = [];
216
+ // Remove existing rafter hooks
217
+ config.hooks.pre_run_command = config.hooks.pre_run_command.filter((entry) => !entry.command?.includes("rafter hook pretool"));
218
+ config.hooks.pre_write_code = config.hooks.pre_write_code.filter((entry) => !entry.command?.includes("rafter hook pretool"));
219
+ config.hooks.pre_run_command.push({
220
+ command: "rafter hook pretool --format windsurf",
221
+ show_output: true,
222
+ });
223
+ config.hooks.pre_write_code.push({
224
+ command: "rafter hook pretool --format windsurf",
225
+ show_output: true,
226
+ });
227
+ fs.writeFileSync(hooksPath, JSON.stringify(config, null, 2), "utf-8");
228
+ console.log(fmt.success(`Installed hooks to ${hooksPath}`));
229
+ }
230
+ function installContinueDevHooks() {
231
+ const homeDir = os.homedir();
232
+ const continueDir = path.join(homeDir, ".continue");
233
+ if (!fs.existsSync(continueDir)) {
234
+ fs.mkdirSync(continueDir, { recursive: true });
235
+ }
236
+ const settingsPath = path.join(continueDir, "settings.json");
237
+ let settings = {};
238
+ if (fs.existsSync(settingsPath)) {
239
+ try {
240
+ settings = JSON.parse(fs.readFileSync(settingsPath, "utf-8"));
241
+ }
242
+ catch {
243
+ console.log(fmt.warning("Existing Continue.dev settings.json was unreadable, creating new one"));
244
+ }
245
+ }
246
+ if (!settings.hooks)
247
+ settings.hooks = {};
248
+ if (!settings.hooks.PreToolUse)
249
+ settings.hooks.PreToolUse = [];
250
+ if (!settings.hooks.PostToolUse)
251
+ settings.hooks.PostToolUse = [];
252
+ // Continue.dev uses the same protocol as Claude Code
253
+ const preHook = { type: "command", command: "rafter hook pretool" };
254
+ const postHook = { type: "command", command: "rafter hook posttool" };
255
+ settings.hooks.PreToolUse = settings.hooks.PreToolUse.filter((entry) => !(entry.hooks || []).some((h) => h.command?.startsWith("rafter hook pretool")));
256
+ settings.hooks.PostToolUse = settings.hooks.PostToolUse.filter((entry) => !(entry.hooks || []).some((h) => h.command?.startsWith("rafter hook posttool")));
257
+ settings.hooks.PreToolUse.push({ matcher: "Bash", hooks: [preHook] }, { matcher: "Write|Edit", hooks: [preHook] });
258
+ settings.hooks.PostToolUse.push({ matcher: ".*", hooks: [postHook] });
259
+ fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), "utf-8");
260
+ console.log(fmt.success(`Installed hooks to ${settingsPath}`));
261
+ }
57
262
  /** MCP server entry for rafter — shared across MCP-native clients */
58
263
  const RAFTER_MCP_ENTRY = {
59
264
  command: "rafter",
@@ -214,10 +419,10 @@ async function installClaudeCodeSkills() {
214
419
  }
215
420
  if (fs.existsSync(backendTemplatePath)) {
216
421
  fs.copyFileSync(backendTemplatePath, backendSkillPath);
217
- console.log(fmt.success(`Installed Rafter Backend skill to ${backendSkillPath}`));
422
+ console.log(fmt.success(`Installed Rafter Remote skill to ${backendSkillPath}`));
218
423
  }
219
424
  else {
220
- console.log(fmt.warning(`Backend skill template not found at ${backendTemplatePath}`));
425
+ console.log(fmt.warning(`Remote skill template not found at ${backendTemplatePath}`));
221
426
  }
222
427
  // Install Agent Security Skill
223
428
  const agentSkillDir = path.join(claudeSkillsDir, "rafter-agent-security");
@@ -246,10 +451,10 @@ function installCodexSkills() {
246
451
  }
247
452
  if (fs.existsSync(backendTemplatePath)) {
248
453
  fs.copyFileSync(backendTemplatePath, backendSkillPath);
249
- console.log(fmt.success(`Installed Rafter Backend skill to ${backendSkillPath}`));
454
+ console.log(fmt.success(`Installed Rafter Remote skill to ${backendSkillPath}`));
250
455
  }
251
456
  else {
252
- console.log(fmt.warning(`Backend skill template not found at ${backendTemplatePath}`));
457
+ console.log(fmt.warning(`Remote skill template not found at ${backendTemplatePath}`));
253
458
  }
254
459
  // Install Agent Security Skill
255
460
  const agentDir = path.join(agentsSkillsDir, "rafter-agent-security");
@@ -266,6 +471,20 @@ function installCodexSkills() {
266
471
  console.log(fmt.warning(`Agent Security skill template not found at ${agentTemplatePath}`));
267
472
  }
268
473
  }
474
+ async function askYesNo(question, defaultYes = true) {
475
+ const rl = createInterface({ input: process.stdin, output: process.stderr });
476
+ const suffix = defaultYes ? "[Y/n]" : "[y/N]";
477
+ return new Promise((resolve) => {
478
+ rl.question(` ${question} ${suffix} `, (answer) => {
479
+ rl.close();
480
+ const trimmed = answer.trim().toLowerCase();
481
+ if (trimmed === "")
482
+ resolve(defaultYes);
483
+ else
484
+ resolve(trimmed === "y" || trimmed === "yes");
485
+ });
486
+ });
487
+ }
269
488
  export function createInitCommand() {
270
489
  return new Command("init")
271
490
  .description("Initialize agent security system")
@@ -280,6 +499,7 @@ export function createInitCommand() {
280
499
  .option("--with-continue", "Install Continue.dev integration")
281
500
  .option("--with-gitleaks", "Download and install Gitleaks binary")
282
501
  .option("--all", "Install all detected integrations and download Gitleaks")
502
+ .option("-i, --interactive", "Guided setup — prompts for each detected integration")
283
503
  .option("--update", "Re-download gitleaks and reinstall integrations without resetting config")
284
504
  .action(async (opts) => {
285
505
  console.log(fmt.header("Rafter Agent Security Setup"));
@@ -295,16 +515,41 @@ export function createInitCommand() {
295
515
  const hasWindsurf = fs.existsSync(path.join(os.homedir(), ".codeium", "windsurf"));
296
516
  const hasContinueDev = fs.existsSync(path.join(os.homedir(), ".continue"));
297
517
  const hasAider = fs.existsSync(path.join(os.homedir(), ".aider.conf.yml"));
298
- // Resolve opt-in flags (--all enables all detected)
299
- const wantOpenClaw = opts.withOpenclaw || opts.all;
300
- const wantClaudeCode = opts.withClaudeCode || opts.all;
301
- const wantCodex = opts.withCodex || opts.all;
302
- const wantGemini = opts.withGemini || opts.all;
303
- const wantCursor = opts.withCursor || opts.all;
304
- const wantWindsurf = opts.withWindsurf || opts.all;
305
- const wantContinue = opts.withContinue || opts.all;
306
- const wantAider = opts.withAider || opts.all;
307
- const wantGitleaks = opts.withGitleaks || opts.all;
518
+ // Resolve opt-in flags (--all enables all detected, --interactive prompts)
519
+ let wantOpenClaw = opts.withOpenclaw || opts.all;
520
+ let wantClaudeCode = opts.withClaudeCode || opts.all;
521
+ let wantCodex = opts.withCodex || opts.all;
522
+ let wantGemini = opts.withGemini || opts.all;
523
+ let wantCursor = opts.withCursor || opts.all;
524
+ let wantWindsurf = opts.withWindsurf || opts.all;
525
+ let wantContinue = opts.withContinue || opts.all;
526
+ let wantAider = opts.withAider || opts.all;
527
+ let wantGitleaks = opts.withGitleaks || opts.all;
528
+ // Interactive mode: prompt for each detected integration
529
+ if (opts.interactive && !opts.all) {
530
+ console.log();
531
+ console.log(fmt.info("Select integrations to install:"));
532
+ console.log();
533
+ if (hasClaudeCode && !wantClaudeCode)
534
+ wantClaudeCode = await askYesNo("Install Claude Code hooks + skills?");
535
+ if (hasCodex && !wantCodex)
536
+ wantCodex = await askYesNo("Install Codex CLI skills + hooks?");
537
+ if (hasOpenClaw && !wantOpenClaw)
538
+ wantOpenClaw = await askYesNo("Install OpenClaw skill?");
539
+ if (hasGemini && !wantGemini)
540
+ wantGemini = await askYesNo("Install Gemini CLI MCP + hooks?");
541
+ if (hasCursor && !wantCursor)
542
+ wantCursor = await askYesNo("Install Cursor MCP + hooks?");
543
+ if (hasWindsurf && !wantWindsurf)
544
+ wantWindsurf = await askYesNo("Install Windsurf MCP + hooks?");
545
+ if (hasContinueDev && !wantContinue)
546
+ wantContinue = await askYesNo("Install Continue.dev MCP + hooks?");
547
+ if (hasAider && !wantAider)
548
+ wantAider = await askYesNo("Install Aider MCP server?");
549
+ if (!wantGitleaks)
550
+ wantGitleaks = await askYesNo("Download Gitleaks binary (enhanced scanning)?");
551
+ console.log();
552
+ }
308
553
  // Show detected environments with opt-in hints
309
554
  const detected = [];
310
555
  if (hasOpenClaw)
@@ -453,8 +698,9 @@ export function createInitCommand() {
453
698
  }
454
699
  }
455
700
  // Install Claude Code skills + hooks if opted in
701
+ // When --with-claude-code is explicitly passed, install even if .claude doesn't exist yet
456
702
  let claudeCodeOk = false;
457
- if (hasClaudeCode && wantClaudeCode) {
703
+ if ((hasClaudeCode || opts.withClaudeCode) && wantClaudeCode) {
458
704
  try {
459
705
  await installClaudeCodeSkills();
460
706
  installClaudeCodeHooks();
@@ -465,11 +711,12 @@ export function createInitCommand() {
465
711
  console.error(fmt.error(`Failed to install Claude Code integration: ${e}`));
466
712
  }
467
713
  }
468
- // Install Codex CLI skills if opted in
714
+ // Install Codex CLI skills + hooks if opted in
469
715
  let codexOk = false;
470
716
  if (hasCodex && wantCodex) {
471
717
  try {
472
718
  installCodexSkills();
719
+ installCodexHooks();
473
720
  manager.set("agent.environments.codex.enabled", true);
474
721
  codexOk = true;
475
722
  }
@@ -477,11 +724,12 @@ export function createInitCommand() {
477
724
  console.error(fmt.error(`Failed to install Codex CLI integration: ${e}`));
478
725
  }
479
726
  }
480
- // Install Gemini CLI MCP if opted in
727
+ // Install Gemini CLI MCP + hooks if opted in
481
728
  let geminiOk = false;
482
729
  if (hasGemini && wantGemini) {
483
730
  try {
484
731
  geminiOk = installGeminiMcp();
732
+ installGeminiHooks();
485
733
  if (geminiOk)
486
734
  manager.set("agent.environments.gemini.enabled", true);
487
735
  }
@@ -489,11 +737,12 @@ export function createInitCommand() {
489
737
  console.error(fmt.error(`Failed to install Gemini CLI integration: ${e}`));
490
738
  }
491
739
  }
492
- // Install Cursor MCP if opted in
740
+ // Install Cursor MCP + hooks if opted in
493
741
  let cursorOk = false;
494
742
  if (hasCursor && wantCursor) {
495
743
  try {
496
744
  cursorOk = installCursorMcp();
745
+ installCursorHooks();
497
746
  if (cursorOk)
498
747
  manager.set("agent.environments.cursor.enabled", true);
499
748
  }
@@ -501,11 +750,12 @@ export function createInitCommand() {
501
750
  console.error(fmt.error(`Failed to install Cursor integration: ${e}`));
502
751
  }
503
752
  }
504
- // Install Windsurf MCP if opted in
753
+ // Install Windsurf MCP + hooks if opted in
505
754
  let windsurfOk = false;
506
755
  if (hasWindsurf && wantWindsurf) {
507
756
  try {
508
757
  windsurfOk = installWindsurfMcp();
758
+ installWindsurfHooks();
509
759
  if (windsurfOk)
510
760
  manager.set("agent.environments.windsurf.enabled", true);
511
761
  }
@@ -513,11 +763,12 @@ export function createInitCommand() {
513
763
  console.error(fmt.error(`Failed to install Windsurf integration: ${e}`));
514
764
  }
515
765
  }
516
- // Install Continue.dev MCP if opted in
766
+ // Install Continue.dev MCP + hooks if opted in
517
767
  let continueOk = false;
518
768
  if (hasContinueDev && wantContinue) {
519
769
  try {
520
770
  continueOk = installContinueDevMcp();
771
+ installContinueDevHooks();
521
772
  if (continueOk)
522
773
  manager.set("agent.environments.continueDev.enabled", true);
523
774
  }
@@ -537,6 +788,11 @@ export function createInitCommand() {
537
788
  console.error(fmt.error(`Failed to install Aider integration: ${e}`));
538
789
  }
539
790
  }
791
+ // Install global instruction files for platforms that support them
792
+ installGlobalInstructions({
793
+ claudeCode: claudeCodeOk,
794
+ cursor: cursorOk,
795
+ });
540
796
  console.log();
541
797
  console.log(fmt.success("Agent security initialized!"));
542
798
  console.log();
@@ -4,6 +4,7 @@ import os from "os";
4
4
  import path from "path";
5
5
  import { execSync } from "child_process";
6
6
  import { fileURLToPath } from 'url';
7
+ import { fmt } from "../../utils/formatter.js";
7
8
  const __filename = fileURLToPath(import.meta.url);
8
9
  const __dirname = path.dirname(__filename);
9
10
  export function createInstallHookCommand() {
@@ -28,7 +29,7 @@ async function installHook(opts) {
28
29
  function getTemplatePath(templateName) {
29
30
  const templatePath = path.join(__dirname, "..", "..", "..", "resources", templateName);
30
31
  if (!fs.existsSync(templatePath)) {
31
- console.error("❌ Error: Hook template not found");
32
+ console.error(fmt.error("Hook template not found"));
32
33
  console.error(` Expected at: ${templatePath}`);
33
34
  process.exit(1);
34
35
  }
@@ -42,7 +43,7 @@ async function installLocalHook(hookName, templateName) {
42
43
  execSync("git rev-parse --git-dir", { stdio: "pipe" });
43
44
  }
44
45
  catch (e) {
45
- console.error("❌ Error: Not in a git repository");
46
+ console.error(fmt.error("Not in a git repository"));
46
47
  console.error(" Run this command from inside a git repository");
47
48
  process.exit(1);
48
49
  }
@@ -56,30 +57,30 @@ async function installLocalHook(hookName, templateName) {
56
57
  const existing = fs.readFileSync(hookPath, "utf-8");
57
58
  const marker = hookName === "pre-push" ? "Rafter Security Pre-Push Hook" : "Rafter Security Pre-Commit Hook";
58
59
  if (existing.includes(marker)) {
59
- console.log(`✓ Rafter ${hookName} hook already installed`);
60
+ console.log(fmt.success(`Rafter ${hookName} hook already installed`));
60
61
  return;
61
62
  }
62
63
  const backupPath = `${hookPath}.backup-${Date.now()}`;
63
64
  fs.copyFileSync(hookPath, backupPath);
64
- console.log(`📦 Backed up existing hook to: ${path.basename(backupPath)}`);
65
+ console.log(fmt.info(`Backed up existing hook to: ${path.basename(backupPath)}`));
65
66
  }
66
67
  const hookContent = fs.readFileSync(getTemplatePath(templateName), "utf-8");
67
68
  fs.writeFileSync(hookPath, hookContent, "utf-8");
68
69
  fs.chmodSync(hookPath, 0o755);
69
- console.log(`✓ Installed Rafter ${hookName} hook`);
70
+ console.log(fmt.success(`Installed Rafter ${hookName} hook`));
70
71
  console.log(` Location: ${hookPath}`);
71
72
  console.log();
72
73
  if (hookName === "pre-push") {
73
74
  console.log("The hook will:");
74
- console.log(" Scan commits being pushed for secrets");
75
- console.log(" Block pushes if secrets are detected");
76
- console.log(" Can be bypassed with: git push --no-verify (not recommended)");
75
+ console.log(" - Scan commits being pushed for secrets");
76
+ console.log(" - Block pushes if secrets are detected");
77
+ console.log(" - Can be bypassed with: git push --no-verify (not recommended)");
77
78
  }
78
79
  else {
79
80
  console.log("The hook will:");
80
- console.log(" Scan staged files for secrets before each commit");
81
- console.log(" Block commits if secrets are detected");
82
- console.log(" Can be bypassed with: git commit --no-verify (not recommended)");
81
+ console.log(" - Scan staged files for secrets before each commit");
82
+ console.log(" - Block commits if secrets are detected");
83
+ console.log(" - Can be bypassed with: git commit --no-verify (not recommended)");
83
84
  }
84
85
  console.log();
85
86
  }
@@ -89,7 +90,7 @@ async function installLocalHook(hookName, templateName) {
89
90
  async function installGlobalHook(hookName, templateName) {
90
91
  const homeDir = os.homedir();
91
92
  if (!homeDir) {
92
- console.error("❌ Error: Could not determine home directory");
93
+ console.error(fmt.error("Could not determine home directory"));
93
94
  process.exit(1);
94
95
  }
95
96
  const globalHooksDir = path.join(homeDir, ".rafter", "git-hooks");
@@ -102,7 +103,7 @@ async function installGlobalHook(hookName, templateName) {
102
103
  fs.chmodSync(hookPath, 0o755);
103
104
  try {
104
105
  execSync(`git config --global core.hooksPath "${globalHooksDir}"`, { stdio: "pipe" });
105
- console.log(`✓ Installed Rafter ${hookName} hook globally`);
106
+ console.log(fmt.success(`Installed Rafter ${hookName} hook globally`));
106
107
  console.log(` Location: ${hookPath}`);
107
108
  console.log(` Git config: core.hooksPath = ${globalHooksDir}`);
108
109
  console.log();
@@ -116,7 +117,7 @@ async function installGlobalHook(hookName, templateName) {
116
117
  console.log();
117
118
  }
118
119
  catch (e) {
119
- console.error("Failed to configure global git hooks");
120
+ console.error(fmt.error("Failed to configure global git hooks"));
120
121
  console.error(" You may need to manually set: git config --global core.hooksPath ~/.rafter/git-hooks");
121
122
  process.exit(1);
122
123
  }
@@ -0,0 +1,63 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ export const RAFTER_MARKER_START = "<!-- rafter:start -->";
4
+ export const RAFTER_MARKER_END = "<!-- rafter:end -->";
5
+ /** Rafter security context block for agent instruction files (CLAUDE.md, AGENTS.md, etc.) */
6
+ export const RAFTER_INSTRUCTION_BLOCK = `${RAFTER_MARKER_START}
7
+ ## Security: Rafter
8
+
9
+ This environment has Rafter available — the security toolkit built for AI workflows.
10
+
11
+ **Local scanning** (always available, no API key needed):
12
+ - \`rafter scan local .\` — scan for leaked credentials
13
+ - \`rafter scan local --staged\` — scan staged changes only
14
+
15
+ **Remote analysis** (requires RAFTER_API_KEY):
16
+ - \`rafter run\` — deterministic SAST, secret detection, dependency checks
17
+ - \`rafter run --mode plus\` — agentic deep-dive with additional analysis passes
18
+
19
+ Consider scanning before pushing, shipping features, or merging PRs.
20
+ If RAFTER_API_KEY is not set, local scanning still works — use what's available.
21
+
22
+ **Project integration:**
23
+ - \`rafter agent install-hook\` — pre-commit secret scanning
24
+ - \`rafter ci init\` — add scanning to CI/CD pipeline
25
+ - \`.rafter.yml\` — project-specific security policy
26
+
27
+ **More:**
28
+ - \`rafter agent audit-skill <path>\` — audit a skill before installing
29
+ - \`rafter agent audit --last 5\` — recent security events
30
+ - \`rafter brief commands\` — full CLI reference
31
+ ${RAFTER_MARKER_END}`;
32
+ /**
33
+ * Write a Rafter instruction block into an instruction file.
34
+ * Uses marker comments for idempotent updates — existing rafter blocks are replaced,
35
+ * non-rafter content is preserved.
36
+ */
37
+ export function injectInstructionFile(filePath) {
38
+ const dir = path.dirname(filePath);
39
+ if (!fs.existsSync(dir)) {
40
+ fs.mkdirSync(dir, { recursive: true });
41
+ }
42
+ const block = RAFTER_INSTRUCTION_BLOCK;
43
+ const startMarker = RAFTER_MARKER_START;
44
+ const endMarker = RAFTER_MARKER_END;
45
+ let content = "";
46
+ if (fs.existsSync(filePath)) {
47
+ content = fs.readFileSync(filePath, "utf-8");
48
+ }
49
+ // Replace existing block or append
50
+ const startIdx = content.indexOf(startMarker);
51
+ const endIdx = content.indexOf(endMarker);
52
+ if (startIdx !== -1 && endIdx !== -1) {
53
+ content = content.slice(0, startIdx) + block + content.slice(endIdx + endMarker.length);
54
+ }
55
+ else {
56
+ if (content.length > 0 && !content.endsWith("\n\n")) {
57
+ content += content.endsWith("\n") ? "\n" : "\n\n";
58
+ }
59
+ content += block + "\n";
60
+ }
61
+ fs.writeFileSync(filePath, content, "utf-8");
62
+ return true;
63
+ }
@@ -47,6 +47,7 @@ export function createScanCommand() {
47
47
  .option("--engine <engine>", "Scan engine: gitleaks or patterns", "auto")
48
48
  .option("--baseline", "Filter findings present in the saved baseline")
49
49
  .option("--watch", "Watch for file changes and re-scan on change")
50
+ .option("--history", "Scan git history for secrets (requires gitleaks engine)")
50
51
  .action(async (scanPath, opts) => {
51
52
  // Validate flags before doing any work
52
53
  const validEngines = ["auto", "gitleaks", "patterns"];
@@ -103,7 +104,7 @@ export function createScanCommand() {
103
104
  if (!opts.quiet) {
104
105
  console.error(`Scanning directory: ${resolvedPath} (${engine})`);
105
106
  }
106
- results = await scanDirectory(resolvedPath, engine, scanCfg);
107
+ results = await scanDirectory(resolvedPath, engine, scanCfg, opts.history);
107
108
  }
108
109
  else {
109
110
  if (!opts.quiet) {
@@ -372,11 +373,11 @@ async function scanFile(filePath, engine, scanCfg) {
372
373
  /**
373
374
  * Scan a directory with selected engine
374
375
  */
375
- async function scanDirectory(dirPath, engine, scanCfg) {
376
+ async function scanDirectory(dirPath, engine, scanCfg, history) {
376
377
  if (engine === "gitleaks") {
377
378
  try {
378
379
  const gitleaks = new GitleaksScanner();
379
- return await gitleaks.scanDirectory(dirPath);
380
+ return await gitleaks.scanDirectory(dirPath, { useGit: history ?? false });
380
381
  }
381
382
  catch (e) {
382
383
  console.error(fmt.warning("Gitleaks scan failed, falling back to patterns"));
@@ -193,7 +193,7 @@ export function createVerifyCommand() {
193
193
  console.log(fmt.success(`${passed.length}/${results.length} core checks passed${warnNote}`));
194
194
  }
195
195
  else {
196
- console.log(fmt.error(`${hardFailed.length} check${hardFailed.length > 1 ? "s" : ""} failed`));
196
+ console.log(fmt.error(`${passed.length}/${results.length} checks passed ${hardFailed.length} failed`));
197
197
  }
198
198
  console.log();
199
199
  if (hardFailed.length > 0) {
@@ -9,6 +9,7 @@ import { handleScanStatus } from "./scan-status.js";
9
9
  */
10
10
  export async function runRemoteScan(opts) {
11
11
  const key = resolveKey(opts.apiKey);
12
+ const ghToken = opts.githubToken || process.env.RAFTER_GITHUB_TOKEN;
12
13
  let repo, branch;
13
14
  try {
14
15
  ({ repo, branch } = detectRepo({ repo: opts.repo, branch: opts.branch, quiet: opts.quiet }));
@@ -22,10 +23,17 @@ export async function runRemoteScan(opts) {
22
23
  }
23
24
  process.exit(EXIT_GENERAL_ERROR);
24
25
  }
26
+ const body = {
27
+ repository_name: repo,
28
+ branch_name: branch,
29
+ scan_mode: opts.mode ?? "fast",
30
+ };
31
+ if (ghToken)
32
+ body.github_token = ghToken;
25
33
  if (!opts.quiet) {
26
34
  const spinner = ora("Submitting scan").start();
27
35
  try {
28
- const { data } = await axios.post(`${API}/static/scan`, { repository_name: repo, branch_name: branch, scan_mode: opts.mode ?? "fast" }, { headers: { "x-api-key": key } });
36
+ const { data } = await axios.post(`${API}/static/scan`, body, { headers: { "x-api-key": key } });
29
37
  spinner.succeed(`Scan ID: ${data.scan_id}`);
30
38
  if (opts.skipInteractive)
31
39
  return;
@@ -56,7 +64,7 @@ export async function runRemoteScan(opts) {
56
64
  }
57
65
  else {
58
66
  try {
59
- const { data } = await axios.post(`${API}/static/scan`, { repository_name: repo, branch_name: branch, scan_mode: opts.mode ?? "fast" }, { headers: { "x-api-key": key } });
67
+ const { data } = await axios.post(`${API}/static/scan`, body, { headers: { "x-api-key": key } });
60
68
  if (opts.skipInteractive)
61
69
  return;
62
70
  const exitCode = await handleScanStatus(data.scan_id, { "x-api-key": key }, opts.format ?? "md", opts.quiet);
@@ -90,12 +98,13 @@ function addRunOptions(cmd) {
90
98
  .option("-k, --api-key <key>", "API key or RAFTER_API_KEY env var")
91
99
  .option("-f, --format <format>", "json | md", "md")
92
100
  .option("-m, --mode <mode>", "scan mode: fast | plus", "fast")
101
+ .option("--github-token <token>", "GitHub PAT for private repos (or RAFTER_GITHUB_TOKEN env var)")
93
102
  .option("--skip-interactive", "do not wait for scan to complete")
94
103
  .option("--quiet", "suppress status messages");
95
104
  }
96
105
  export function createRunCommand() {
97
106
  return addRunOptions(new Command("run")
98
- .description("Trigger a remote backend security scan")).action(async (opts) => {
107
+ .description("Trigger a remote security scan")).action(async (opts) => {
99
108
  await runRemoteScan(opts);
100
109
  });
101
110
  }