@rafter-security/cli 0.6.5 → 0.6.6

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",
@@ -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)
@@ -465,11 +710,12 @@ export function createInitCommand() {
465
710
  console.error(fmt.error(`Failed to install Claude Code integration: ${e}`));
466
711
  }
467
712
  }
468
- // Install Codex CLI skills if opted in
713
+ // Install Codex CLI skills + hooks if opted in
469
714
  let codexOk = false;
470
715
  if (hasCodex && wantCodex) {
471
716
  try {
472
717
  installCodexSkills();
718
+ installCodexHooks();
473
719
  manager.set("agent.environments.codex.enabled", true);
474
720
  codexOk = true;
475
721
  }
@@ -477,11 +723,12 @@ export function createInitCommand() {
477
723
  console.error(fmt.error(`Failed to install Codex CLI integration: ${e}`));
478
724
  }
479
725
  }
480
- // Install Gemini CLI MCP if opted in
726
+ // Install Gemini CLI MCP + hooks if opted in
481
727
  let geminiOk = false;
482
728
  if (hasGemini && wantGemini) {
483
729
  try {
484
730
  geminiOk = installGeminiMcp();
731
+ installGeminiHooks();
485
732
  if (geminiOk)
486
733
  manager.set("agent.environments.gemini.enabled", true);
487
734
  }
@@ -489,11 +736,12 @@ export function createInitCommand() {
489
736
  console.error(fmt.error(`Failed to install Gemini CLI integration: ${e}`));
490
737
  }
491
738
  }
492
- // Install Cursor MCP if opted in
739
+ // Install Cursor MCP + hooks if opted in
493
740
  let cursorOk = false;
494
741
  if (hasCursor && wantCursor) {
495
742
  try {
496
743
  cursorOk = installCursorMcp();
744
+ installCursorHooks();
497
745
  if (cursorOk)
498
746
  manager.set("agent.environments.cursor.enabled", true);
499
747
  }
@@ -501,11 +749,12 @@ export function createInitCommand() {
501
749
  console.error(fmt.error(`Failed to install Cursor integration: ${e}`));
502
750
  }
503
751
  }
504
- // Install Windsurf MCP if opted in
752
+ // Install Windsurf MCP + hooks if opted in
505
753
  let windsurfOk = false;
506
754
  if (hasWindsurf && wantWindsurf) {
507
755
  try {
508
756
  windsurfOk = installWindsurfMcp();
757
+ installWindsurfHooks();
509
758
  if (windsurfOk)
510
759
  manager.set("agent.environments.windsurf.enabled", true);
511
760
  }
@@ -513,11 +762,12 @@ export function createInitCommand() {
513
762
  console.error(fmt.error(`Failed to install Windsurf integration: ${e}`));
514
763
  }
515
764
  }
516
- // Install Continue.dev MCP if opted in
765
+ // Install Continue.dev MCP + hooks if opted in
517
766
  let continueOk = false;
518
767
  if (hasContinueDev && wantContinue) {
519
768
  try {
520
769
  continueOk = installContinueDevMcp();
770
+ installContinueDevHooks();
521
771
  if (continueOk)
522
772
  manager.set("agent.environments.continueDev.enabled", true);
523
773
  }
@@ -537,6 +787,11 @@ export function createInitCommand() {
537
787
  console.error(fmt.error(`Failed to install Aider integration: ${e}`));
538
788
  }
539
789
  }
790
+ // Install global instruction files for platforms that support them
791
+ installGlobalInstructions({
792
+ claudeCode: claudeCodeOk,
793
+ cursor: cursorOk,
794
+ });
540
795
  console.log();
541
796
  console.log(fmt.success("Agent security initialized!"));
542
797
  console.log();
@@ -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
+ }
@@ -128,6 +128,43 @@ function buildTopics() {
128
128
  description: "Setup instructions for unsupported / generic agents",
129
129
  render: () => renderPlatformSetup("generic"),
130
130
  },
131
+ pricing: {
132
+ description: "What's free, what's paid, and the philosophy behind it",
133
+ render: () => [
134
+ "# Rafter Pricing",
135
+ "",
136
+ "**Free forever for individuals and open source. No account required. No telemetry.**",
137
+ "",
138
+ "## What's Free",
139
+ "",
140
+ "All local agent security features are free with no limits:",
141
+ "",
142
+ "- Secret scanning (21+ patterns, Gitleaks integration)",
143
+ "- Pre-commit hooks (local and global)",
144
+ "- Command interception with risk-tiered approval",
145
+ "- Skill/extension auditing",
146
+ "- Audit logging",
147
+ "- MCP server for tool integration",
148
+ "- CI/CD pipeline generation",
149
+ "- All supported agent integrations (Claude Code, Codex, Gemini, Cursor, Windsurf, Aider, OpenClaw, Continue.dev)",
150
+ "",
151
+ "No API key. No sign-up. No telemetry. No data collection. No network access required.",
152
+ "Everything runs locally on your machine. MIT licensed.",
153
+ "",
154
+ "## Remote Code Analysis (API)",
155
+ "",
156
+ "Remote SAST/SCA scanning via the Rafter API has a free tier.",
157
+ "Sign up at rafter.so for an API key. Enterprise plans offer higher",
158
+ "limits, dashboards, policy management, and compliance reporting.",
159
+ "",
160
+ "## Philosophy",
161
+ "",
162
+ "Security tooling should be free for the people writing code.",
163
+ "Generous free tiers drive bottom-up adoption. Enterprise value",
164
+ "comes from dashboards, policy, and compliance — not from gating",
165
+ "the tools developers use every day.",
166
+ ].join("\n"),
167
+ },
131
168
  all: {
132
169
  description: "Everything — full security + scanning + setup briefing",
133
170
  render: () => {
@@ -47,7 +47,7 @@ export function createCiInitCommand() {
47
47
  if (platform === "github") {
48
48
  console.log();
49
49
  console.log("Alternatives:");
50
- console.log(" - GitHub Action: uses: Raftersecurity/rafter-cli@v0");
50
+ console.log(" - GitHub Action: uses: Raftersecurity/rafter-cli@v1");
51
51
  console.log(" - Pre-commit: https://github.com/Raftersecurity/rafter-cli#pre-commit-framework");
52
52
  }
53
53
  console.log();
@@ -74,7 +74,7 @@ function generateTemplate(platform, withBackend) {
74
74
  }
75
75
  function githubTemplate(withBackend) {
76
76
  let yaml = `# Generated by: rafter ci init
77
- # Alternative: uses: Raftersecurity/rafter-cli@v0
77
+ # Alternative: uses: Raftersecurity/rafter-cli@v1
78
78
  name: Rafter Security
79
79
 
80
80
  on:
@@ -4,31 +4,71 @@ import { AuditLogger } from "../../core/audit-logger.js";
4
4
  export function createHookPosttoolCommand() {
5
5
  return new Command("posttool")
6
6
  .description("PostToolUse hook handler (reads stdin, redacts secrets in output, writes JSON to stdout)")
7
- .action(async () => {
7
+ .option("--format <format>", "Output format: claude (default, also Codex/Continue), cursor, gemini, windsurf", "claude")
8
+ .action(async (opts) => {
9
+ const format = (opts.format || "claude");
8
10
  try {
9
11
  const input = await readStdin();
10
- let payload;
12
+ let raw;
11
13
  try {
12
- payload = JSON.parse(input);
14
+ raw = JSON.parse(input);
13
15
  }
14
16
  catch {
15
- writeOutput({ action: "continue" });
17
+ writeOutput({ action: "continue" }, format);
16
18
  return;
17
19
  }
18
20
  // Validate payload is an object with expected shape
19
- if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
20
- writeOutput({ action: "continue" });
21
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
22
+ writeOutput({ action: "continue" }, format);
21
23
  return;
22
24
  }
25
+ const payload = normalizePostInput(raw, format);
23
26
  const output = evaluateToolResponse(payload);
24
- writeOutput(output);
27
+ writeOutput(output, format);
25
28
  }
26
29
  catch {
27
30
  // Any unexpected error → fail open
28
- writeOutput({ action: "continue" });
31
+ writeOutput({ action: "continue" }, format);
29
32
  }
30
33
  });
31
34
  }
35
+ /**
36
+ * Normalize platform-specific PostToolUse stdin into common shape.
37
+ * Windsurf sends { tool_info: { stdout, stderr } }, Cursor sends { output, ... }.
38
+ */
39
+ function normalizePostInput(raw, format) {
40
+ if (format === "windsurf") {
41
+ const toolInfo = raw.tool_info || {};
42
+ return {
43
+ session_id: raw.trajectory_id,
44
+ tool_name: raw.agent_action_name?.includes("run_command") ? "Bash" : (toolInfo.mcp_tool_name || "unknown"),
45
+ tool_input: {},
46
+ tool_response: {
47
+ output: toolInfo.stdout || toolInfo.output || "",
48
+ error: toolInfo.stderr || "",
49
+ },
50
+ };
51
+ }
52
+ if (format === "cursor") {
53
+ return {
54
+ session_id: raw.conversation_id,
55
+ tool_name: raw.hook_event_name === "afterShellExecution" ? "Bash" : (raw.tool_name || "unknown"),
56
+ tool_input: raw.tool_input || {},
57
+ tool_response: {
58
+ output: raw.output || raw.tool_response?.output || "",
59
+ content: raw.content || raw.tool_response?.content || "",
60
+ error: raw.error || raw.tool_response?.error || "",
61
+ },
62
+ };
63
+ }
64
+ // Claude, Codex, Continue, Gemini — same shape
65
+ return {
66
+ session_id: raw.session_id,
67
+ tool_name: raw.tool_name || "",
68
+ tool_input: raw.tool_input || {},
69
+ tool_response: raw.tool_response,
70
+ };
71
+ }
32
72
  function evaluateToolResponse(payload) {
33
73
  const { tool_response } = payload;
34
74
  // No response body — pass through
@@ -82,6 +122,51 @@ function readStdin() {
82
122
  process.stdin.resume();
83
123
  });
84
124
  }
85
- function writeOutput(output) {
86
- process.stdout.write(JSON.stringify(output) + "\n");
125
+ function writeOutput(output, format) {
126
+ const isModify = output.action === "modify" && output.tool_response;
127
+ switch (format) {
128
+ case "cursor": {
129
+ // Cursor: { agentMessage?: string } for post-tool notifications
130
+ if (isModify) {
131
+ process.stdout.write(JSON.stringify({
132
+ agentMessage: "Rafter redacted secrets from tool output",
133
+ }) + "\n");
134
+ }
135
+ // No output for continue (noop)
136
+ break;
137
+ }
138
+ case "gemini": {
139
+ // Gemini AfterTool: { systemMessage?: string } or {}
140
+ if (isModify) {
141
+ process.stdout.write(JSON.stringify({
142
+ systemMessage: "Rafter redacted secrets from tool output",
143
+ }) + "\n");
144
+ }
145
+ else {
146
+ process.stdout.write("{}\n");
147
+ }
148
+ break;
149
+ }
150
+ case "windsurf": {
151
+ // Windsurf: exit 0 for continue, stderr for notification
152
+ if (isModify) {
153
+ process.stderr.write("Rafter: secrets redacted from tool output\n");
154
+ }
155
+ // Always exit 0 for post-tool (never block after execution)
156
+ break;
157
+ }
158
+ default: {
159
+ // Claude Code / Codex / Continue.dev: hookSpecificOutput envelope
160
+ const hookOutput = {
161
+ hookSpecificOutput: {
162
+ hookEventName: "PostToolUse",
163
+ },
164
+ };
165
+ if (isModify) {
166
+ hookOutput.hookSpecificOutput.modifiedToolResult = output.tool_response;
167
+ }
168
+ process.stdout.write(JSON.stringify(hookOutput) + "\n");
169
+ break;
170
+ }
171
+ }
87
172
  }