@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.
- package/README.md +25 -25
- package/dist/commands/agent/audit-skill.js +20 -19
- package/dist/commands/agent/config.js +2 -1
- package/dist/commands/agent/exec.js +2 -0
- package/dist/commands/agent/index.js +2 -0
- package/dist/commands/agent/init-project.js +164 -0
- package/dist/commands/agent/init.js +276 -20
- package/dist/commands/agent/install-hook.js +15 -14
- package/dist/commands/agent/instruction-block.js +63 -0
- package/dist/commands/agent/scan.js +4 -3
- package/dist/commands/agent/verify.js +1 -1
- package/dist/commands/backend/run.js +12 -3
- package/dist/commands/backend/scan-status.js +3 -2
- package/dist/commands/brief.js +39 -2
- package/dist/commands/ci/init.js +26 -22
- package/dist/commands/completion.js +4 -3
- package/dist/commands/hook/posttool.js +95 -10
- package/dist/commands/hook/pretool.js +105 -10
- package/dist/commands/mcp/server.js +5 -5
- package/dist/commands/notify.js +278 -0
- package/dist/commands/report.js +274 -0
- package/dist/commands/scan/index.js +7 -5
- package/dist/core/risk-rules.js +18 -3
- package/dist/index.js +20 -10
- package/dist/scanners/gitleaks.js +14 -4
- package/dist/scanners/secret-patterns.js +1 -1
- package/package.json +2 -1
- package/resources/pre-commit-hook.sh +0 -5
- package/resources/rafter-security-skill.md +1 -1
- package/resources/skills/rafter/SKILL.md +25 -6
- package/resources/skills/rafter-agent-security/SKILL.md +25 -35
|
@@ -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
|
|
422
|
+
console.log(fmt.success(`Installed Rafter Remote skill to ${backendSkillPath}`));
|
|
218
423
|
}
|
|
219
424
|
else {
|
|
220
|
-
console.log(fmt.warning(`
|
|
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
|
|
454
|
+
console.log(fmt.success(`Installed Rafter Remote skill to ${backendSkillPath}`));
|
|
250
455
|
}
|
|
251
456
|
else {
|
|
252
|
-
console.log(fmt.warning(`
|
|
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
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
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("
|
|
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("
|
|
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(
|
|
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(
|
|
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(
|
|
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("
|
|
75
|
-
console.log("
|
|
76
|
-
console.log("
|
|
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("
|
|
81
|
-
console.log("
|
|
82
|
-
console.log("
|
|
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("
|
|
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(
|
|
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("
|
|
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(`${
|
|
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`,
|
|
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`,
|
|
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
|
|
107
|
+
.description("Trigger a remote security scan")).action(async (opts) => {
|
|
99
108
|
await runRemoteScan(opts);
|
|
100
109
|
});
|
|
101
110
|
}
|