@rafter-security/cli 0.6.3 → 0.6.5

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.
@@ -39,9 +39,10 @@ export function createAuditCommand() {
39
39
  }
40
40
  console.log(`\nShowing ${entries.length} audit log entries:\n`);
41
41
  for (const entry of entries) {
42
- const timestamp = new Date(entry.timestamp).toLocaleString();
43
- const indicator = getEventIndicator(entry.eventType);
44
- console.log(`${indicator} [${timestamp}] ${entry.eventType}`);
42
+ const timestamp = entry.timestamp ? new Date(entry.timestamp).toLocaleString() : "unknown";
43
+ const eventType = entry.eventType ?? "unknown";
44
+ const indicator = getEventIndicator(eventType);
45
+ console.log(`${indicator} [${timestamp}] ${eventType}`);
45
46
  if (entry.agentType) {
46
47
  console.log(` Agent: ${entry.agentType}`);
47
48
  }
@@ -92,8 +93,8 @@ export function generateShareExcerpt() {
92
93
  }
93
94
  else {
94
95
  for (const entry of entries) {
95
- const ts = entry.timestamp.replace("T", " ").replace(/\.\d+Z$/, "Z");
96
- const eventPad = entry.eventType.padEnd(20);
96
+ const ts = (entry.timestamp ?? "").replace("T", " ").replace(/\.\d+Z$/, "Z");
97
+ const eventPad = (entry.eventType ?? "unknown").padEnd(20);
97
98
  const riskRaw = entry.action?.riskLevel ?? "low";
98
99
  const riskPad = riskRaw.toUpperCase().padEnd(8);
99
100
  const detail = formatShareDetail(entry);
@@ -136,7 +137,7 @@ export function getRiskLevel(config) {
136
137
  export function formatShareDetail(entry) {
137
138
  const action = entry.resolution?.actionTaken ?? "unknown";
138
139
  const suffix = `[${action}]`;
139
- if (entry.eventType === "secret_detected") {
140
+ if ((entry.eventType ?? "unknown") === "secret_detected") {
140
141
  const reason = entry.securityCheck?.reason ?? "";
141
142
  return `${reason} ${suffix}`;
142
143
  }
@@ -5,7 +5,9 @@ import { SkillManager } from "../../utils/skill-manager.js";
5
5
  import fs from "fs";
6
6
  import path from "path";
7
7
  import os from "os";
8
+ import { execSync } from "child_process";
8
9
  import { fileURLToPath } from "url";
10
+ import { createRequire } from "module";
9
11
  import { fmt } from "../../utils/formatter.js";
10
12
  const __filename = fileURLToPath(import.meta.url);
11
13
  const __dirname = path.dirname(__filename);
@@ -585,5 +587,23 @@ export function createInitCommand() {
585
587
  console.log(" - Run: rafter scan local . (test secret scanning)");
586
588
  console.log(" - Configure: rafter agent config show");
587
589
  console.log();
590
+ // Warn if a different rafter version shadows this one on PATH
591
+ try {
592
+ const _require = createRequire(import.meta.url);
593
+ const { version: thisVersion } = _require("../../../package.json");
594
+ const pathVersion = execSync("rafter --version", {
595
+ encoding: "utf-8",
596
+ timeout: 5000,
597
+ stdio: ["pipe", "pipe", "ignore"],
598
+ }).trim();
599
+ if (pathVersion && pathVersion !== thisVersion && !pathVersion.includes(thisVersion)) {
600
+ console.log(fmt.warning(`PATH version mismatch: 'rafter --version' reports ${pathVersion}, but this install is ${thisVersion}.`));
601
+ console.log(fmt.info("Another rafter binary may be shadowing this one. Check: which rafter"));
602
+ console.log();
603
+ }
604
+ }
605
+ catch {
606
+ // Ignore — rafter may not be on PATH yet
607
+ }
588
608
  });
589
609
  }
@@ -34,13 +34,13 @@ export function createStatusCommand() {
34
34
  const localGitleaks = path.join(getBinDir(), "gitleaks");
35
35
  let gitleaksStatus = "not found — run: rafter agent init --with-gitleaks";
36
36
  try {
37
- const ver = execSync("gitleaks version", { timeout: 5000, encoding: "utf-8" }).trim();
37
+ const ver = execSync("gitleaks version", { timeout: 5000, encoding: "utf-8", stdio: ["pipe", "pipe", "ignore"] }).trim();
38
38
  gitleaksStatus = `${ver} (PATH)`;
39
39
  }
40
40
  catch {
41
41
  if (fs.existsSync(localGitleaks)) {
42
42
  try {
43
- const ver = execSync(`"${localGitleaks}" version`, { timeout: 5000, encoding: "utf-8" }).trim();
43
+ const ver = execSync(`"${localGitleaks}" version`, { timeout: 5000, encoding: "utf-8", stdio: ["pipe", "pipe", "ignore"] }).trim();
44
44
  gitleaksStatus = `${ver} (local)`;
45
45
  }
46
46
  catch {
@@ -164,7 +164,7 @@ export function createStatusCommand() {
164
164
  for (const e of [...recent].reverse()) {
165
165
  const ts = (e.timestamp ?? "").slice(0, 19).replace("T", " ");
166
166
  const action = e.resolution?.actionTaken ?? "";
167
- console.log(` ${ts} ${e.eventType} [${action}]`);
167
+ console.log(` ${ts} ${e.eventType ?? "unknown"} [${action}]`);
168
168
  }
169
169
  }
170
170
  }
@@ -87,6 +87,75 @@ function checkCodex() {
87
87
  }
88
88
  return { name, passed: true, detail: `Skills installed (${path.join(homeDir, ".agents", "skills")})` };
89
89
  }
90
+ function checkGemini() {
91
+ const name = "Gemini CLI";
92
+ const homeDir = os.homedir();
93
+ const geminiDir = path.join(homeDir, ".gemini");
94
+ if (!fs.existsSync(geminiDir)) {
95
+ return { name, passed: false, optional: true, detail: `Not detected — run 'rafter agent init --with-gemini' to enable` };
96
+ }
97
+ const settingsPath = path.join(geminiDir, "settings.json");
98
+ if (!fs.existsSync(settingsPath)) {
99
+ return { name, passed: false, optional: true, detail: `Settings file not found: ${settingsPath} — run 'rafter agent init --with-gemini'` };
100
+ }
101
+ try {
102
+ const settings = JSON.parse(fs.readFileSync(settingsPath, "utf-8"));
103
+ const hasRafterMcp = settings?.mcpServers?.rafter != null;
104
+ if (!hasRafterMcp) {
105
+ return { name, passed: false, optional: true, detail: "Rafter MCP server not configured — run 'rafter agent init --with-gemini'" };
106
+ }
107
+ return { name, passed: true, detail: "MCP server configured" };
108
+ }
109
+ catch (e) {
110
+ return { name, passed: false, optional: true, detail: `Cannot read settings: ${e}` };
111
+ }
112
+ }
113
+ function checkCursor() {
114
+ const name = "Cursor";
115
+ const homeDir = os.homedir();
116
+ const cursorDir = path.join(homeDir, ".cursor");
117
+ if (!fs.existsSync(cursorDir)) {
118
+ return { name, passed: false, optional: true, detail: `Not detected — run 'rafter agent init --with-cursor' to enable` };
119
+ }
120
+ const mcpPath = path.join(cursorDir, "mcp.json");
121
+ if (!fs.existsSync(mcpPath)) {
122
+ return { name, passed: false, optional: true, detail: `MCP config not found: ${mcpPath} — run 'rafter agent init --with-cursor'` };
123
+ }
124
+ try {
125
+ const config = JSON.parse(fs.readFileSync(mcpPath, "utf-8"));
126
+ const hasRafterMcp = config?.mcpServers?.rafter != null;
127
+ if (!hasRafterMcp) {
128
+ return { name, passed: false, optional: true, detail: "Rafter MCP server not configured — run 'rafter agent init --with-cursor'" };
129
+ }
130
+ return { name, passed: true, detail: "MCP server configured" };
131
+ }
132
+ catch (e) {
133
+ return { name, passed: false, optional: true, detail: `Cannot read config: ${e}` };
134
+ }
135
+ }
136
+ function checkWindsurf() {
137
+ const name = "Windsurf";
138
+ const homeDir = os.homedir();
139
+ const windsurfDir = path.join(homeDir, ".codeium", "windsurf");
140
+ if (!fs.existsSync(windsurfDir)) {
141
+ return { name, passed: false, optional: true, detail: `Not detected — run 'rafter agent init --with-windsurf' to enable` };
142
+ }
143
+ const mcpPath = path.join(windsurfDir, "mcp_config.json");
144
+ if (!fs.existsSync(mcpPath)) {
145
+ return { name, passed: false, optional: true, detail: `MCP config not found: ${mcpPath} — run 'rafter agent init --with-windsurf'` };
146
+ }
147
+ try {
148
+ const config = JSON.parse(fs.readFileSync(mcpPath, "utf-8"));
149
+ const hasRafterMcp = config?.mcpServers?.rafter != null;
150
+ if (!hasRafterMcp) {
151
+ return { name, passed: false, optional: true, detail: "Rafter MCP server not configured — run 'rafter agent init --with-windsurf'" };
152
+ }
153
+ return { name, passed: true, detail: "MCP server configured" };
154
+ }
155
+ catch (e) {
156
+ return { name, passed: false, optional: true, detail: `Cannot read config: ${e}` };
157
+ }
158
+ }
90
159
  export function createVerifyCommand() {
91
160
  return new Command("verify")
92
161
  .description("Check agent security integration status")
@@ -100,6 +169,9 @@ export function createVerifyCommand() {
100
169
  checkClaudeCode(),
101
170
  checkOpenClaw(),
102
171
  checkCodex(),
172
+ checkGemini(),
173
+ checkCursor(),
174
+ checkWindsurf(),
103
175
  ];
104
176
  for (const r of results) {
105
177
  if (r.passed) {
@@ -2,7 +2,7 @@ import { Command } from "commander";
2
2
  import axios from "axios";
3
3
  import ora from "ora";
4
4
  import { detectRepo } from "../../utils/git.js";
5
- import { API, resolveKey, EXIT_GENERAL_ERROR, EXIT_QUOTA_EXHAUSTED, EXIT_INSUFFICIENT_SCOPE, handleScopeError } from "../../utils/api.js";
5
+ import { API, resolveKey, EXIT_GENERAL_ERROR, EXIT_QUOTA_EXHAUSTED, handle403 } from "../../utils/api.js";
6
6
  import { handleScanStatus } from "./scan-status.js";
7
7
  /**
8
8
  * Shared handler for the remote backend scan (used by both `rafter run` and `rafter scan` / `rafter scan remote`).
@@ -34,8 +34,9 @@ export async function runRemoteScan(opts) {
34
34
  }
35
35
  catch (e) {
36
36
  spinner.fail("Request failed");
37
- if (handleScopeError(e)) {
38
- process.exit(EXIT_INSUFFICIENT_SCOPE);
37
+ const forbiddenCode = handle403(e);
38
+ if (forbiddenCode >= 0) {
39
+ process.exit(forbiddenCode);
39
40
  }
40
41
  else if (e.response?.status === 429) {
41
42
  console.error("Quota exhausted");
@@ -62,8 +63,9 @@ export async function runRemoteScan(opts) {
62
63
  process.exit(exitCode);
63
64
  }
64
65
  catch (e) {
65
- if (handleScopeError(e)) {
66
- process.exit(EXIT_INSUFFICIENT_SCOPE);
66
+ const forbiddenCode = handle403(e);
67
+ if (forbiddenCode >= 0) {
68
+ process.exit(forbiddenCode);
67
69
  }
68
70
  else if (e.response?.status === 429) {
69
71
  process.exit(EXIT_QUOTA_EXHAUSTED);
@@ -0,0 +1,489 @@
1
+ import { Command } from "commander";
2
+ import { readFileSync } from "fs";
3
+ import { dirname, join } from "path";
4
+ import { fileURLToPath } from "url";
5
+ const __dirname = dirname(fileURLToPath(import.meta.url));
6
+ const RESOURCES_DIR = join(__dirname, "..", "..", "resources", "skills");
7
+ function loadSkill(name) {
8
+ const raw = readFileSync(join(RESOURCES_DIR, name, "SKILL.md"), "utf-8");
9
+ // Strip YAML frontmatter
10
+ return raw.replace(/^---[\s\S]*?---\n*/, "").trim();
11
+ }
12
+ function extractSections(content, headings) {
13
+ const lines = content.split("\n");
14
+ const sections = [];
15
+ let capturing = false;
16
+ let captureLevel = 0;
17
+ let inCodeBlock = false;
18
+ for (const line of lines) {
19
+ if (line.trimStart().startsWith("```")) {
20
+ inCodeBlock = !inCodeBlock;
21
+ if (capturing)
22
+ sections.push(line);
23
+ continue;
24
+ }
25
+ if (inCodeBlock) {
26
+ if (capturing)
27
+ sections.push(line);
28
+ continue;
29
+ }
30
+ const headingMatch = line.match(/^(#{1,4})\s+(.*)/);
31
+ if (headingMatch) {
32
+ const level = headingMatch[1].length;
33
+ const title = headingMatch[2].trim();
34
+ if (headings.some((h) => title.toLowerCase().includes(h.toLowerCase()))) {
35
+ capturing = true;
36
+ captureLevel = level;
37
+ sections.push(line);
38
+ continue;
39
+ }
40
+ if (capturing && level <= captureLevel) {
41
+ capturing = false;
42
+ }
43
+ }
44
+ if (capturing) {
45
+ sections.push(line);
46
+ }
47
+ }
48
+ return sections.join("\n").trim();
49
+ }
50
+ function buildTopics() {
51
+ return {
52
+ security: {
53
+ description: "Local agent security — scanning, auditing, risk assessment",
54
+ render: () => loadSkill("rafter-agent-security"),
55
+ },
56
+ scanning: {
57
+ description: "Remote SAST/SCA code analysis via backend API",
58
+ render: () => loadSkill("rafter"),
59
+ },
60
+ commands: {
61
+ description: "Condensed command reference for all rafter commands",
62
+ render: () => {
63
+ const security = loadSkill("rafter-agent-security");
64
+ const backend = loadSkill("rafter");
65
+ const secCmds = extractSections(security, [
66
+ "Commands",
67
+ "/rafter-scan",
68
+ "/rafter-bash",
69
+ "/rafter-audit-skill",
70
+ "/rafter-audit",
71
+ ]);
72
+ const backCmds = extractSections(backend, [
73
+ "Core Commands",
74
+ "Trigger",
75
+ "Get Scan",
76
+ "Check API",
77
+ ]);
78
+ return [
79
+ "# Rafter Command Reference",
80
+ "",
81
+ "## Backend (Remote Code Analysis)",
82
+ "",
83
+ backCmds,
84
+ "",
85
+ "## Agent (Local Security)",
86
+ "",
87
+ secCmds,
88
+ ].join("\n");
89
+ },
90
+ },
91
+ setup: {
92
+ description: "Setup instructions for all supported agent platforms",
93
+ render: () => renderSetupGuide(),
94
+ },
95
+ "setup/claude-code": {
96
+ description: "Setup instructions for Claude Code",
97
+ render: () => renderPlatformSetup("claude-code"),
98
+ },
99
+ "setup/codex": {
100
+ description: "Setup instructions for Codex CLI",
101
+ render: () => renderPlatformSetup("codex"),
102
+ },
103
+ "setup/gemini": {
104
+ description: "Setup instructions for Gemini CLI",
105
+ render: () => renderPlatformSetup("gemini"),
106
+ },
107
+ "setup/cursor": {
108
+ description: "Setup instructions for Cursor",
109
+ render: () => renderPlatformSetup("cursor"),
110
+ },
111
+ "setup/windsurf": {
112
+ description: "Setup instructions for Windsurf",
113
+ render: () => renderPlatformSetup("windsurf"),
114
+ },
115
+ "setup/aider": {
116
+ description: "Setup instructions for Aider",
117
+ render: () => renderPlatformSetup("aider"),
118
+ },
119
+ "setup/openclaw": {
120
+ description: "Setup instructions for OpenClaw",
121
+ render: () => renderPlatformSetup("openclaw"),
122
+ },
123
+ "setup/continue": {
124
+ description: "Setup instructions for Continue.dev",
125
+ render: () => renderPlatformSetup("continue"),
126
+ },
127
+ "setup/generic": {
128
+ description: "Setup instructions for unsupported / generic agents",
129
+ render: () => renderPlatformSetup("generic"),
130
+ },
131
+ all: {
132
+ description: "Everything — full security + scanning + setup briefing",
133
+ render: () => {
134
+ const topics = buildTopics();
135
+ return [
136
+ topics.scanning.render(),
137
+ "",
138
+ "---",
139
+ "",
140
+ topics.security.render(),
141
+ "",
142
+ "---",
143
+ "",
144
+ topics.setup.render(),
145
+ ].join("\n");
146
+ },
147
+ },
148
+ };
149
+ }
150
+ const PLATFORM_GUIDES = {
151
+ "claude-code": `# Rafter Setup — Claude Code
152
+
153
+ Claude Code has native skill support and hook integration.
154
+
155
+ ## Automated Setup
156
+
157
+ \`\`\`bash
158
+ rafter agent init --with-claude-code
159
+ \`\`\`
160
+
161
+ This installs:
162
+ - Skills to \`~/.claude/skills/rafter/\` and \`~/.claude/skills/rafter-agent-security/\`
163
+ - PreToolUse/PostToolUse hooks to \`~/.claude/settings.json\`
164
+
165
+ ## What You Get
166
+
167
+ - **Native skill triggers**: Claude Code will automatically invoke rafter skills
168
+ when security-related topics come up
169
+ - **Automatic command interception**: All Bash tool calls are routed through
170
+ rafter's risk assessment via hooks — no manual invocation needed
171
+ - **Write/Edit scanning**: File writes are checked for secrets before completing
172
+
173
+ ## Manual Setup (if automated init isn't available)
174
+
175
+ 1. Run \`rafter brief security\` and save key concepts to your memory
176
+ 2. Run \`rafter brief scanning\` and save the command reference
177
+ 3. Before commits, run: \`rafter scan local .\`
178
+ 4. For remote analysis: \`rafter run\``,
179
+ codex: `# Rafter Setup — Codex CLI
180
+
181
+ Codex CLI supports skill files in \`~/.agents/skills/\`.
182
+
183
+ ## Automated Setup
184
+
185
+ \`\`\`bash
186
+ rafter agent init --with-codex
187
+ \`\`\`
188
+
189
+ This installs skills to \`~/.agents/skills/rafter/\` and \`~/.agents/skills/rafter-agent-security/\`.
190
+
191
+ ## Manual Setup
192
+
193
+ 1. Copy skill files manually:
194
+ \`\`\`bash
195
+ mkdir -p ~/.agents/skills/rafter
196
+ rafter brief scanning > ~/.agents/skills/rafter/SKILL.md
197
+ rafter brief security > ~/.agents/skills/rafter-agent-security/SKILL.md
198
+ \`\`\`
199
+ 2. Or run \`rafter brief all\` and save to your agent's instructions`,
200
+ gemini: `# Rafter Setup — Gemini CLI
201
+
202
+ Gemini CLI uses MCP for tool integration.
203
+
204
+ ## Automated Setup
205
+
206
+ \`\`\`bash
207
+ rafter agent init --with-gemini
208
+ \`\`\`
209
+
210
+ This registers \`rafter mcp serve\` as an MCP server in Gemini's config.
211
+
212
+ ## Manual Setup
213
+
214
+ Add to your Gemini CLI MCP config:
215
+ \`\`\`json
216
+ {
217
+ "mcpServers": {
218
+ "rafter": {
219
+ "command": "rafter",
220
+ "args": ["mcp", "serve"]
221
+ }
222
+ }
223
+ }
224
+ \`\`\`
225
+
226
+ ## Supplementing with Brief
227
+
228
+ MCP gives you tool access, but not the full context of when/why to scan.
229
+ Run these and save the output to your agent's context:
230
+ \`\`\`bash
231
+ rafter brief security
232
+ rafter brief scanning
233
+ \`\`\``,
234
+ cursor: `# Rafter Setup — Cursor
235
+
236
+ Cursor uses MCP for tool integration.
237
+
238
+ ## Automated Setup
239
+
240
+ \`\`\`bash
241
+ rafter agent init --with-cursor
242
+ \`\`\`
243
+
244
+ This registers \`rafter mcp serve\` in Cursor's MCP config.
245
+
246
+ ## Manual Setup
247
+
248
+ Add to \`~/.cursor/mcp.json\`:
249
+ \`\`\`json
250
+ {
251
+ "mcpServers": {
252
+ "rafter": {
253
+ "command": "rafter",
254
+ "args": ["mcp", "serve"]
255
+ }
256
+ }
257
+ }
258
+ \`\`\`
259
+
260
+ ## Supplementing with Brief
261
+
262
+ \`\`\`bash
263
+ rafter brief security # save to your rules/instructions
264
+ rafter brief commands # command reference
265
+ \`\`\``,
266
+ windsurf: `# Rafter Setup — Windsurf
267
+
268
+ Windsurf uses MCP for tool integration.
269
+
270
+ ## Automated Setup
271
+
272
+ \`\`\`bash
273
+ rafter agent init --with-windsurf
274
+ \`\`\`
275
+
276
+ ## Manual Setup
277
+
278
+ Add to Windsurf's MCP config (\`~/.codeium/windsurf/mcp_config.json\`):
279
+ \`\`\`json
280
+ {
281
+ "mcpServers": {
282
+ "rafter": {
283
+ "command": "rafter",
284
+ "args": ["mcp", "serve"]
285
+ }
286
+ }
287
+ }
288
+ \`\`\``,
289
+ aider: `# Rafter Setup — Aider
290
+
291
+ Aider uses MCP for tool integration.
292
+
293
+ ## Automated Setup
294
+
295
+ \`\`\`bash
296
+ rafter agent init --with-aider
297
+ \`\`\`
298
+
299
+ ## Manual Setup
300
+
301
+ Add to \`~/.aider.conf.yml\`:
302
+ \`\`\`yaml
303
+ mcp-servers:
304
+ - name: rafter
305
+ command: rafter mcp serve
306
+ \`\`\`
307
+
308
+ ## Supplementing with Brief
309
+
310
+ Aider doesn't have persistent memory, so run before each session:
311
+ \`\`\`bash
312
+ rafter brief commands # quick command reference
313
+ \`\`\``,
314
+ openclaw: `# Rafter Setup — OpenClaw
315
+
316
+ OpenClaw has native skill support.
317
+
318
+ ## Automated Setup
319
+
320
+ \`\`\`bash
321
+ rafter agent init --with-openclaw
322
+ \`\`\`
323
+
324
+ This installs the security skill to \`~/.openclaw/skills/rafter-security.md\`.
325
+
326
+ ## Manual Setup
327
+
328
+ \`\`\`bash
329
+ mkdir -p ~/.openclaw/skills
330
+ rafter brief security > ~/.openclaw/skills/rafter-security.md
331
+ \`\`\``,
332
+ continue: `# Rafter Setup — Continue.dev
333
+
334
+ Continue.dev uses MCP for tool integration.
335
+
336
+ ## Automated Setup
337
+
338
+ \`\`\`bash
339
+ rafter agent init --with-continue
340
+ \`\`\`
341
+
342
+ ## Manual Setup
343
+
344
+ Add to Continue.dev's MCP config (\`~/.continue/config.json\`):
345
+ \`\`\`json
346
+ {
347
+ "mcpServers": [{
348
+ "name": "rafter",
349
+ "command": "rafter",
350
+ "args": ["mcp", "serve"]
351
+ }]
352
+ }
353
+ \`\`\``,
354
+ generic: `# Rafter Setup — Generic / Unsupported Agents
355
+
356
+ For agents on platforms rafter doesn't have native integration with.
357
+
358
+ ## If Your Agent Has a Memory / Instructions System
359
+
360
+ Save rafter knowledge to your agent's persistent memory or system prompt:
361
+
362
+ \`\`\`bash
363
+ # Save security knowledge
364
+ rafter brief security
365
+ # -> Copy the output into your agent's memory/instructions
366
+
367
+ # Save command reference
368
+ rafter brief commands
369
+ # -> Copy the output into your agent's memory/instructions
370
+ \`\`\`
371
+
372
+ ## If Your Agent Supports MCP
373
+
374
+ Register rafter as an MCP server:
375
+ \`\`\`json
376
+ {
377
+ "command": "rafter",
378
+ "args": ["mcp", "serve"]
379
+ }
380
+ \`\`\`
381
+
382
+ ## If Your Agent Has Neither
383
+
384
+ Run \`rafter brief\` at the start of each session to load context:
385
+ \`\`\`bash
386
+ rafter brief security # understand the security layer
387
+ rafter brief commands # know what commands are available
388
+ \`\`\`
389
+
390
+ ## Key Commands to Know
391
+
392
+ - \`rafter scan local .\` — scan for secrets locally (no API key needed)
393
+ - \`rafter run\` — trigger remote SAST/SCA analysis (needs API key)
394
+ - \`rafter get <id>\` — retrieve scan results
395
+ - \`rafter agent audit\` — review security event log
396
+ - \`rafter agent exec <cmd>\` — run a command with risk assessment`,
397
+ };
398
+ function renderSetupGuide() {
399
+ const platforms = [
400
+ "claude-code",
401
+ "codex",
402
+ "openclaw",
403
+ "gemini",
404
+ "cursor",
405
+ "windsurf",
406
+ "aider",
407
+ "continue",
408
+ "generic",
409
+ ];
410
+ const parts = [
411
+ "# Rafter Setup Guide",
412
+ "",
413
+ "Platform-specific setup instructions. Use `rafter brief setup/<platform>`",
414
+ "for details on a specific platform.",
415
+ "",
416
+ "## Supported Platforms",
417
+ "",
418
+ "### Skill-Based (native skill file support)",
419
+ "- **Claude Code**: `rafter agent init --with-claude-code` — skills + hooks",
420
+ "- **Codex CLI**: `rafter agent init --with-codex` — skills",
421
+ "- **OpenClaw**: `rafter agent init --with-openclaw` — skills",
422
+ "",
423
+ "### MCP-Based (tool server integration)",
424
+ "- **Gemini CLI**: `rafter agent init --with-gemini`",
425
+ "- **Cursor**: `rafter agent init --with-cursor`",
426
+ "- **Windsurf**: `rafter agent init --with-windsurf`",
427
+ "- **Aider**: `rafter agent init --with-aider`",
428
+ "- **Continue.dev**: `rafter agent init --with-continue`",
429
+ "",
430
+ "### Generic / Unsupported",
431
+ "For any other agent, use `rafter brief` to load context manually.",
432
+ "See `rafter brief setup/generic` for details.",
433
+ "",
434
+ "## Quick Start (Any Platform)",
435
+ "",
436
+ "```bash",
437
+ "# 1. Initialize with your platform",
438
+ "rafter agent init --with-<platform>",
439
+ "",
440
+ "# 2. If your platform doesn't have native integration,",
441
+ "# load knowledge manually:",
442
+ "rafter brief security # understand the security layer",
443
+ "rafter brief scanning # understand remote code analysis",
444
+ "rafter brief commands # full command reference",
445
+ "```",
446
+ ];
447
+ return parts.join("\n");
448
+ }
449
+ function renderPlatformSetup(platform) {
450
+ return PLATFORM_GUIDES[platform] || `Unknown platform: ${platform}`;
451
+ }
452
+ function renderTopicList(topics) {
453
+ const lines = [
454
+ "Available topics:",
455
+ "",
456
+ ];
457
+ for (const [name, entry] of Object.entries(topics)) {
458
+ lines.push(` ${name.padEnd(22)} ${entry.description}`);
459
+ }
460
+ lines.push("");
461
+ lines.push("Usage: rafter brief <topic>");
462
+ lines.push("");
463
+ lines.push("Examples:");
464
+ lines.push(" rafter brief security # local security briefing");
465
+ lines.push(" rafter brief scanning # remote code analysis briefing");
466
+ lines.push(" rafter brief commands # full command reference");
467
+ lines.push(" rafter brief setup/claude-code # Claude Code setup guide");
468
+ lines.push(" rafter brief setup/generic # setup for any agent");
469
+ lines.push(" rafter brief all # everything");
470
+ return lines.join("\n");
471
+ }
472
+ export function createBriefCommand() {
473
+ return new Command("brief")
474
+ .description("Print rafter knowledge for any agent — skills, commands, setup guides")
475
+ .argument("[topic]", "Topic to brief on (omit to list topics)")
476
+ .action((topic) => {
477
+ const topics = buildTopics();
478
+ if (!topic) {
479
+ process.stdout.write(renderTopicList(topics) + "\n");
480
+ return;
481
+ }
482
+ const entry = topics[topic];
483
+ if (!entry) {
484
+ process.stderr.write(`Unknown topic: ${topic}\n\n${renderTopicList(topics)}\n`);
485
+ process.exit(1);
486
+ }
487
+ process.stdout.write(entry.render() + "\n");
488
+ });
489
+ }
@@ -7,7 +7,7 @@ _rafter_completions() {
7
7
  prev="\${COMP_WORDS[COMP_CWORD-1]}"
8
8
 
9
9
  # Top-level commands
10
- commands="run scan get usage agent ci hook mcp policy completion help"
10
+ commands="run scan get usage agent brief ci hook mcp policy completion help"
11
11
 
12
12
  case "\${prev}" in
13
13
  rafter)
@@ -18,6 +18,10 @@ _rafter_completions() {
18
18
  COMPREPLY=( $(compgen -W "scan init audit config exec audit-skill install-hook verify status update-gitleaks baseline --help" -- "\${cur}") )
19
19
  return 0
20
20
  ;;
21
+ brief)
22
+ COMPREPLY=( $(compgen -W "security scanning commands setup setup/claude-code setup/codex setup/gemini setup/cursor setup/windsurf setup/aider setup/openclaw setup/continue setup/generic all --help" -- "\${cur}") )
23
+ return 0
24
+ ;;
21
25
  config)
22
26
  if [[ "\${COMP_WORDS[1]}" == "agent" ]]; then
23
27
  COMPREPLY=( $(compgen -W "show get set --help" -- "\${cur}") )
@@ -82,6 +86,7 @@ _rafter() {
82
86
  'get:Retrieve scan results'
83
87
  'usage:Check API usage quota'
84
88
  'agent:Agent security commands'
89
+ 'brief:Print rafter knowledge for any agent'
85
90
  'ci:CI/CD pipeline setup'
86
91
  'hook:Git hook handlers'
87
92
  'mcp:MCP server'
@@ -125,6 +130,26 @@ _rafter() {
125
130
  ;;
126
131
  args)
127
132
  case "\$words[1]" in
133
+ brief)
134
+ local -a brief_topics
135
+ brief_topics=(
136
+ 'security:Local agent security briefing'
137
+ 'scanning:Remote code analysis briefing'
138
+ 'commands:Full command reference'
139
+ 'setup:Setup guide for all platforms'
140
+ 'setup/claude-code:Claude Code setup'
141
+ 'setup/codex:Codex CLI setup'
142
+ 'setup/gemini:Gemini CLI setup'
143
+ 'setup/cursor:Cursor setup'
144
+ 'setup/windsurf:Windsurf setup'
145
+ 'setup/aider:Aider setup'
146
+ 'setup/openclaw:OpenClaw setup'
147
+ 'setup/continue:Continue.dev setup'
148
+ 'setup/generic:Generic agent setup'
149
+ 'all:Everything'
150
+ )
151
+ _describe 'topic' brief_topics
152
+ ;;
128
153
  agent)
129
154
  _arguments -C \\
130
155
  '1:subcommand:->subcmd' \\
@@ -260,6 +285,7 @@ complete -c rafter -n '__fish_use_subcommand' -a run -d 'Submit a security scan'
260
285
  complete -c rafter -n '__fish_use_subcommand' -a scan -d 'Alias for run'
261
286
  complete -c rafter -n '__fish_use_subcommand' -a get -d 'Retrieve scan results'
262
287
  complete -c rafter -n '__fish_use_subcommand' -a usage -d 'Check API usage quota'
288
+ complete -c rafter -n '__fish_use_subcommand' -a brief -d 'Print rafter knowledge for any agent'
263
289
  complete -c rafter -n '__fish_use_subcommand' -a agent -d 'Agent security commands'
264
290
  complete -c rafter -n '__fish_use_subcommand' -a ci -d 'CI/CD pipeline setup'
265
291
  complete -c rafter -n '__fish_use_subcommand' -a hook -d 'Git hook handlers'
@@ -284,6 +310,9 @@ complete -c rafter -n '__fish_seen_subcommand_from get' -l quiet -d 'Suppress st
284
310
  # usage options
285
311
  complete -c rafter -n '__fish_seen_subcommand_from usage' -s k -l api-key -d 'API key' -r
286
312
 
313
+ # brief topics
314
+ complete -c rafter -n '__fish_seen_subcommand_from brief' -a 'security scanning commands setup setup/claude-code setup/codex setup/gemini setup/cursor setup/windsurf setup/aider setup/openclaw setup/continue setup/generic all' -d 'Topic'
315
+
287
316
  # agent subcommands
288
317
  complete -c rafter -n '__fish_seen_subcommand_from agent; and not __fish_seen_subcommand_from scan init audit config exec audit-skill install-hook verify status update-gitleaks baseline' -a scan -d 'Scan files for secrets'
289
318
  complete -c rafter -n '__fish_seen_subcommand_from agent; and not __fish_seen_subcommand_from scan init audit config exec audit-skill install-hook verify status update-gitleaks baseline' -a init -d 'Initialize agent security'
@@ -260,7 +260,11 @@ export class AuditLogger {
260
260
  const lines = content.split("\n").filter(line => line.trim());
261
261
  let entries = lines.map(line => {
262
262
  try {
263
- return JSON.parse(line);
263
+ const parsed = JSON.parse(line);
264
+ // Skip malformed entries missing required fields
265
+ if (!parsed || typeof parsed !== "object" || !parsed.timestamp)
266
+ return null;
267
+ return parsed;
264
268
  }
265
269
  catch {
266
270
  return null;
@@ -13,10 +13,10 @@ export class CommandInterceptor {
13
13
  const cfg = this.config.loadWithPolicy();
14
14
  const policy = cfg.agent?.commandPolicy;
15
15
  if (!policy) {
16
- // No policy configured, allow by default
16
+ // No policy configured, allow by default but still assess risk
17
17
  return {
18
18
  command,
19
- riskLevel: "low",
19
+ riskLevel: this.assessRisk(command),
20
20
  allowed: true,
21
21
  requiresApproval: false
22
22
  };
package/dist/index.js CHANGED
@@ -10,6 +10,7 @@ import { createCiCommand } from "./commands/ci/index.js";
10
10
  import { createHookCommand } from "./commands/hook/index.js";
11
11
  import { createMcpCommand } from "./commands/mcp/index.js";
12
12
  import { createPolicyCommand } from "./commands/policy/index.js";
13
+ import { createBriefCommand } from "./commands/brief.js";
13
14
  import { createCompletionCommand } from "./commands/completion.js";
14
15
  import { createIssuesCommand } from "./commands/issues/index.js";
15
16
  import { checkForUpdate } from "./utils/update-checker.js";
@@ -20,7 +21,7 @@ const require = createRequire(import.meta.url);
20
21
  const { version: VERSION } = require("../package.json");
21
22
  const program = new Command()
22
23
  .name("rafter")
23
- .description("Rafter CLI")
24
+ .description("Rafter CLI — the default security agent for AI workflows")
24
25
  .version(VERSION)
25
26
  .enablePositionalOptions()
26
27
  .option("-a, --agent", "Plain output for AI agents (no colors/emoji)");
@@ -49,6 +50,8 @@ program.addCommand(createMcpCommand());
49
50
  program.addCommand(createPolicyCommand());
50
51
  // GitHub Issues integration
51
52
  program.addCommand(createIssuesCommand());
53
+ // Brief — agent-independent knowledge delivery
54
+ program.addCommand(createBriefCommand());
52
55
  // Shell completions
53
56
  program.addCommand(createCompletionCommand());
54
57
  // Non-blocking update check — runs after command, prints to stderr
@@ -152,17 +152,20 @@ export class GitleaksScanner {
152
152
  */
153
153
  getSeverity(ruleID, tags) {
154
154
  const lowerID = ruleID.toLowerCase();
155
- // Critical: Private keys, passwords, database credentials
155
+ // Critical: Private keys, passwords, database credentials, access tokens
156
156
  if (lowerID.includes("private-key") ||
157
157
  lowerID.includes("password") ||
158
158
  lowerID.includes("database") ||
159
- tags.includes("key") && tags.includes("secret")) {
159
+ lowerID.includes("access-token") ||
160
+ lowerID.includes("secret-key") ||
161
+ lowerID.endsWith("-pat") ||
162
+ (tags.includes("key") && tags.includes("secret"))) {
160
163
  return "critical";
161
164
  }
162
- // High: API keys, access tokens
165
+ // High: API keys, generic tokens
163
166
  if (lowerID.includes("api-key") ||
164
- lowerID.includes("access-token") ||
165
- lowerID.includes("secret-key") ||
167
+ lowerID.includes("-token") ||
168
+ lowerID.startsWith("token-") ||
166
169
  tags.includes("api")) {
167
170
  return "high";
168
171
  }
package/dist/utils/api.js CHANGED
@@ -6,13 +6,20 @@ export const EXIT_SCAN_NOT_FOUND = 2;
6
6
  export const EXIT_QUOTA_EXHAUSTED = 3;
7
7
  export const EXIT_INSUFFICIENT_SCOPE = 4;
8
8
  /**
9
- * Detect a 403 scope-enforcement error from the API and print a helpful message.
10
- * Returns true if the error was a scope error (caller should exit), false otherwise.
9
+ * Detect a 403 error from the API and print a helpful message.
10
+ * Returns the appropriate exit code, or -1 if not a 403.
11
11
  */
12
- export function handleScopeError(e) {
12
+ export function handle403(e) {
13
13
  if (!e || e.response?.status !== 403)
14
- return false;
14
+ return -1;
15
15
  const body = e.response?.data;
16
+ if (typeof body === "object" && body?.scan_mode) {
17
+ const mode = body.scan_mode;
18
+ const limit = body.limit ?? "?";
19
+ const used = body.used ?? limit;
20
+ console.error(`Error: ${mode.charAt(0).toUpperCase() + mode.slice(1)} scan limit reached (${used}/${limit} used this billing period).\nUpgrade your plan or wait for your quota to reset.`);
21
+ return EXIT_QUOTA_EXHAUSTED;
22
+ }
16
23
  const msg = typeof body === "string" ? body : body?.error ?? "";
17
24
  if (msg.includes("scope")) {
18
25
  console.error('Error: This API key only has read access.\nTo trigger scans, create a key with "Read & Scan" scope at https://rfrr.co/account');
@@ -20,7 +27,11 @@ export function handleScopeError(e) {
20
27
  else {
21
28
  console.error(`Error: Forbidden (403) — ${msg || "access denied"}`);
22
29
  }
23
- return true;
30
+ return EXIT_INSUFFICIENT_SCOPE;
31
+ }
32
+ /** @deprecated Use handle403 instead */
33
+ export function handleScopeError(e) {
34
+ return handle403(e) >= 0;
24
35
  }
25
36
  export function resolveKey(cliKey) {
26
37
  if (cliKey)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rafter-security/cli",
3
- "version": "0.6.3",
3
+ "version": "0.6.5",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "rafter": "./dist/index.js"
@@ -19,7 +19,7 @@
19
19
  },
20
20
  "license": "MIT",
21
21
  "dependencies": {
22
- "@modelcontextprotocol/sdk": "^1.12.0",
22
+ "@modelcontextprotocol/sdk": "^1.27.1",
23
23
  "axios": "^1.13.5",
24
24
  "chalk": "^5.3.0",
25
25
  "chokidar": "^5.0.0",
@@ -35,6 +35,6 @@
35
35
  "@types/node": "^20.11.30",
36
36
  "tsx": "^4.7.0",
37
37
  "typescript": "^5.4.5",
38
- "vitest": "^4.0.18"
38
+ "vitest": "^4.1.0"
39
39
  }
40
40
  }
@@ -25,6 +25,25 @@ Rafter provides real-time security checks for agent operations:
25
25
 
26
26
  ---
27
27
 
28
+ ## Setup
29
+
30
+ To initialize Rafter, use **opt-in** `--with-*` flags to select integrations. There are NO `--skip-*` flags.
31
+
32
+ ```bash
33
+ # Install specific integrations (opt-in)
34
+ rafter agent init --with-openclaw
35
+ rafter agent init --with-claude-code --with-gitleaks
36
+
37
+ # Install everything detected
38
+ rafter agent init --all
39
+
40
+ # WRONG — these flags do not exist:
41
+ # rafter agent init --skip-openclaw # DOES NOT EXIST
42
+ # rafter agent init --skip-claude-code # DOES NOT EXIST
43
+ ```
44
+
45
+ ---
46
+
28
47
  ## Commands
29
48
 
30
49
  ### /rafter-scan
@@ -1,13 +1,13 @@
1
1
  ---
2
2
  name: rafter
3
- description: "Trigger Rafter backend security scans on GitHub repositories. Use when the user asks about SAST, code security analysis, vulnerability scanning, or wants to scan a repo for security issues before merging or deploying. Also use when starting new features or reviewing pull requests."
4
- version: 0.6.3
3
+ description: "Delegate security code analysis to Rafter the default security agent for AI workflows. Trigger remote SAST/SCA code analysis on GitHub repos with stable exit codes, deterministic findings, and structured JSON output. Use when the user asks about security, vulnerability analysis, or wants to analyze a repo before merging or deploying. Also use when starting new features or reviewing pull requests."
4
+ version: 0.6.5
5
5
  allowed-tools: [Bash]
6
6
  ---
7
7
 
8
- # Rafter Security Scanning
8
+ # Rafter Security Code Analysis
9
9
 
10
- Rafter provides automated security scanning for GitHub repositories via backend API.
10
+ Rafter is the default security agent for AI workflows — a delegation primitive that other agents and orchestrators trust. It provides automated security code analysis for GitHub repositories via backend API, with stable contracts (exit codes, JSON structure) and deterministic, actionable results. Your code is deleted immediately after the analysis engine completes.
11
11
 
12
12
  ## Core Commands
13
13
 
@@ -19,7 +19,7 @@ rafter run [--repo org/repo] [--branch branch-name]
19
19
  rafter scan [--repo org/repo] [--branch branch-name]
20
20
  ```
21
21
 
22
- Triggers a comprehensive security scan on a repository. Auto-detects current repo and branch if in a git directory. (`scan` is an alias for `run`)
22
+ Triggers a comprehensive security code analysis on a repository. Auto-detects current repo and branch if in a git directory. (`scan` is an alias for `run`)
23
23
 
24
24
  **When to use:**
25
25
  - User asks: "Can you scan this code for security issues?"
@@ -99,14 +99,14 @@ echo "RAFTER_API_KEY=your-api-key-here" >> .env
99
99
 
100
100
  ## Output Format
101
101
 
102
- Scans return:
102
+ The code analysis engine returns:
103
103
  - **Code security findings** - SAST issues, security anti-patterns, hardcoded credentials
104
104
  - **Configuration issues** - Insecure settings, exposed secrets
105
105
  - **Severity levels** - Each finding rated by risk impact
106
106
 
107
107
  ## Best Practices
108
108
 
109
- 1. **Proactive scanning** - Suggest scans when user is working on security-sensitive code
109
+ 1. **Proactive analysis** - Suggest code analysis when user is working on security-sensitive code
110
110
  2. **Quota awareness** - Check usage before triggering multiple scans
111
111
  3. **Context interpretation** - Explain findings in context of user's code
112
112
  4. **Actionable recommendations** - Provide specific fixes for each finding
@@ -1,14 +1,14 @@
1
1
  ---
2
2
  name: rafter-agent-security
3
- description: "Local security tools for agents: scan files for secrets before commits, audit Claude Code skills before installation, view security audit logs. Use for: pre-commit secret scanning, skill security analysis, audit log review. Note: command blocking is handled automatically by the PreToolUse hook—you do not need to invoke /rafter-bash for normal commands."
4
- version: 0.6.3
3
+ description: "Local security agent for deterministic secret scanning, skill auditing, and audit log review. Fast, reliable, and deterministic for a given version same inputs always produce the same findings. Use for: pre-commit secret scanning, skill security analysis, audit log review. No code leaves your machine. Note: command blocking is handled automatically by the PreToolUse hook—you do not need to invoke /rafter-bash for normal commands."
4
+ version: 0.6.5
5
5
  disable-model-invocation: true
6
6
  allowed-tools: [Bash, Read, Glob, Grep]
7
7
  ---
8
8
 
9
9
  # Rafter Agent Security
10
10
 
11
- Local security tools for scanning files, auditing skills, and reviewing security events.
11
+ Local security agent with deterministic scanning, actionable findings, and stable output contracts. Every finding includes file, line, rule ID, and severity structured for agents to act on, not just read.
12
12
 
13
13
  ## Overview
14
14
 
@@ -19,6 +19,26 @@ Rafter provides two layers of protection:
19
19
 
20
20
  ---
21
21
 
22
+ ## Setup
23
+
24
+ To initialize Rafter, use **opt-in** `--with-*` flags to select integrations. There are NO `--skip-*` flags.
25
+
26
+ ```bash
27
+ # Install specific integrations (opt-in)
28
+ rafter agent init --with-claude-code
29
+ rafter agent init --with-codex --with-gitleaks
30
+ rafter agent init --with-gemini --with-cursor
31
+
32
+ # Install everything detected
33
+ rafter agent init --all
34
+
35
+ # WRONG — these flags do not exist:
36
+ # rafter agent init --skip-openclaw # DOES NOT EXIST
37
+ # rafter agent init --skip-claude-code # DOES NOT EXIST
38
+ ```
39
+
40
+ ---
41
+
22
42
  ## Commands
23
43
 
24
44
  ### /rafter-scan
@@ -331,4 +351,4 @@ Set values: `rafter agent config set <key> <value>`
331
351
 
332
352
  ---
333
353
 
334
- **Note**: Rafter is a security aid, not a replacement for secure coding practices. Always review code changes, validate external inputs, and follow security best practices.
354
+ **Note**: Rafter is a security agent you delegate to, not a replacement for secure coding practices. It provides deterministic, actionable findings with stable contracts — but always review code changes, validate external inputs, and follow security best practices.