@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.
- package/README.md +22 -22
- package/dist/commands/agent/index.js +2 -0
- package/dist/commands/agent/init-project.js +164 -0
- package/dist/commands/agent/init.js +270 -15
- package/dist/commands/agent/instruction-block.js +63 -0
- package/dist/commands/brief.js +37 -0
- package/dist/commands/ci/init.js +2 -2
- 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 +273 -0
- package/dist/core/risk-rules.js +4 -2
- package/dist/index.js +7 -1
- package/dist/scanners/gitleaks.js +8 -2
- 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 +24 -5
- package/resources/skills/rafter-agent-security/SKILL.md +24 -34
|
@@ -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
|
-
|
|
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)
|
|
@@ -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
|
+
}
|
package/dist/commands/brief.js
CHANGED
|
@@ -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: () => {
|
package/dist/commands/ci/init.js
CHANGED
|
@@ -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@
|
|
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@
|
|
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
|
-
.
|
|
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
|
|
12
|
+
let raw;
|
|
11
13
|
try {
|
|
12
|
-
|
|
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 (!
|
|
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
|
-
|
|
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
|
}
|