@rafter-security/cli 0.5.5 → 0.6.1

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.
Files changed (40) hide show
  1. package/README.md +15 -3
  2. package/dist/commands/agent/audit-skill.js +1 -1
  3. package/dist/commands/agent/audit.js +106 -6
  4. package/dist/commands/agent/baseline.js +10 -0
  5. package/dist/commands/agent/exec.js +1 -1
  6. package/dist/commands/agent/init.js +366 -26
  7. package/dist/commands/agent/scan.js +160 -16
  8. package/dist/commands/agent/status.js +65 -4
  9. package/dist/commands/agent/verify.js +18 -4
  10. package/dist/commands/backend/run.js +76 -62
  11. package/dist/commands/ci/init.js +10 -3
  12. package/dist/commands/completion.js +21 -9
  13. package/dist/commands/hook/posttool.js +21 -7
  14. package/dist/commands/hook/pretool.js +50 -13
  15. package/dist/commands/issues/dedup.js +39 -0
  16. package/dist/commands/issues/from-scan.js +143 -0
  17. package/dist/commands/issues/from-text.js +185 -0
  18. package/dist/commands/issues/github-client.js +85 -0
  19. package/dist/commands/issues/index.js +25 -0
  20. package/dist/commands/issues/issue-builder.js +101 -0
  21. package/dist/commands/mcp/server.js +4 -1
  22. package/dist/commands/policy/export.js +7 -2
  23. package/dist/commands/scan/index.js +45 -0
  24. package/dist/core/audit-logger.js +106 -7
  25. package/dist/core/config-defaults.js +24 -0
  26. package/dist/core/config-manager.js +116 -3
  27. package/dist/core/custom-patterns.js +20 -17
  28. package/dist/core/pattern-engine.js +26 -1
  29. package/dist/core/policy-loader.js +25 -2
  30. package/dist/index.js +11 -2
  31. package/dist/scanners/gitleaks.js +8 -7
  32. package/dist/scanners/regex-scanner.js +16 -1
  33. package/dist/scanners/secret-patterns.js +6 -6
  34. package/dist/utils/api.js +18 -0
  35. package/dist/utils/binary-manager.js +74 -7
  36. package/dist/utils/skill-manager.js +5 -3
  37. package/package.json +5 -3
  38. package/resources/pre-commit-hook.sh +2 -2
  39. package/resources/pre-push-hook.sh +2 -2
  40. package/resources/rafter-security-skill.md +7 -11
@@ -136,10 +136,33 @@ function validatePolicy(policy, raw) {
136
136
  }
137
137
  }
138
138
  if (policy.scan.customPatterns !== undefined) {
139
- if (!Array.isArray(policy.scan.customPatterns) || !policy.scan.customPatterns.every((v) => v && typeof v === "object" && typeof v.name === "string" && v.name !== "" && typeof v.regex === "string" && v.regex !== "" && typeof v.severity === "string")) {
140
- console.error(`Warning: "scan.custom_patterns" must be an array of objects with name, regex, severity — ignoring.`);
139
+ if (!Array.isArray(policy.scan.customPatterns)) {
140
+ console.error(`Warning: "scan.custom_patterns" must be an array — ignoring.`);
141
141
  delete policy.scan.customPatterns;
142
142
  }
143
+ else {
144
+ const valid = [];
145
+ for (const v of policy.scan.customPatterns) {
146
+ if (!v || typeof v !== "object" || typeof v.name !== "string" || !v.name || typeof v.regex !== "string" || !v.regex || typeof v.severity !== "string") {
147
+ console.error(`Warning: skipping malformed custom_patterns entry — must have name, regex, severity.`);
148
+ continue;
149
+ }
150
+ try {
151
+ new RegExp(v.regex);
152
+ }
153
+ catch {
154
+ console.error(`Warning: skipping custom pattern "${v.name}" — invalid regex.`);
155
+ continue;
156
+ }
157
+ valid.push(v);
158
+ }
159
+ if (valid.length > 0) {
160
+ policy.scan.customPatterns = valid;
161
+ }
162
+ else {
163
+ delete policy.scan.customPatterns;
164
+ }
165
+ }
143
166
  }
144
167
  }
145
168
  if (policy.audit) {
package/dist/index.js CHANGED
@@ -4,20 +4,25 @@ import * as dotenv from "dotenv";
4
4
  import { createRunCommand } from "./commands/backend/run.js";
5
5
  import { createGetCommand } from "./commands/backend/get.js";
6
6
  import { createUsageCommand } from "./commands/backend/usage.js";
7
+ import { createScanGroupCommand } from "./commands/scan/index.js";
7
8
  import { createAgentCommand } from "./commands/agent/index.js";
8
9
  import { createCiCommand } from "./commands/ci/index.js";
9
10
  import { createHookCommand } from "./commands/hook/index.js";
10
11
  import { createMcpCommand } from "./commands/mcp/index.js";
11
12
  import { createPolicyCommand } from "./commands/policy/index.js";
12
13
  import { createCompletionCommand } from "./commands/completion.js";
14
+ import { createIssuesCommand } from "./commands/issues/index.js";
13
15
  import { checkForUpdate } from "./utils/update-checker.js";
14
16
  import { setAgentMode } from "./utils/formatter.js";
17
+ import { createRequire } from "module";
15
18
  dotenv.config();
16
- const VERSION = "0.5.5";
19
+ const require = createRequire(import.meta.url);
20
+ const { version: VERSION } = require("../package.json");
17
21
  const program = new Command()
18
22
  .name("rafter")
19
23
  .description("Rafter CLI")
20
24
  .version(VERSION)
25
+ .enablePositionalOptions()
21
26
  .option("-a, --agent", "Plain output for AI agents (no colors/emoji)");
22
27
  // Set agent mode before any subcommand runs
23
28
  program.hook("preAction", (thisCommand) => {
@@ -26,10 +31,12 @@ program.hook("preAction", (thisCommand) => {
26
31
  setAgentMode(true);
27
32
  }
28
33
  });
29
- // Backend commands (existing)
34
+ // Backend commands
30
35
  program.addCommand(createRunCommand());
31
36
  program.addCommand(createGetCommand());
32
37
  program.addCommand(createUsageCommand());
38
+ // Scan command group (default: remote backend scan; subcommands: local, remote)
39
+ program.addCommand(createScanGroupCommand());
33
40
  // Agent commands
34
41
  program.addCommand(createAgentCommand());
35
42
  // CI commands
@@ -40,6 +47,8 @@ program.addCommand(createHookCommand());
40
47
  program.addCommand(createMcpCommand());
41
48
  // Policy commands
42
49
  program.addCommand(createPolicyCommand());
50
+ // GitHub Issues integration
51
+ program.addCommand(createIssuesCommand());
43
52
  // Shell completions
44
53
  program.addCommand(createCompletionCommand());
45
54
  // Non-blocking update check — runs after command, prints to stderr
@@ -1,5 +1,6 @@
1
1
  import { execFile } from "child_process";
2
2
  import { promisify } from "util";
3
+ import { randomBytes } from "crypto";
3
4
  import { BinaryManager } from "../utils/binary-manager.js";
4
5
  import fs from "fs";
5
6
  import os from "os";
@@ -26,7 +27,7 @@ export class GitleaksScanner {
26
27
  throw new Error("Gitleaks not available");
27
28
  }
28
29
  const gitleaksPath = this.binaryManager.getGitleaksPath();
29
- const tmpReport = path.join(os.tmpdir(), `gitleaks-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.json`);
30
+ const tmpReport = path.join(os.tmpdir(), `gitleaks-${Date.now()}-${randomBytes(6).toString("hex")}.json`);
30
31
  try {
31
32
  // Run gitleaks detect on file
32
33
  await execFileAsync(gitleaksPath, ["detect", "--no-git", "-f", "json", "-r", tmpReport, "-s", filePath], { timeout: 30000 });
@@ -44,11 +45,7 @@ export class GitleaksScanner {
44
45
  };
45
46
  }
46
47
  catch (e) {
47
- // Clean up report
48
- if (fs.existsSync(tmpReport)) {
49
- fs.unlinkSync(tmpReport);
50
- }
51
- // Gitleaks exits with code 1 when leaks found
48
+ // Gitleaks exits with code 1 when leaks found — read report before cleanup
52
49
  if (e.code === 1 && fs.existsSync(tmpReport)) {
53
50
  const results = this.parseResults(tmpReport);
54
51
  fs.unlinkSync(tmpReport);
@@ -57,6 +54,10 @@ export class GitleaksScanner {
57
54
  matches: results.map(r => this.convertToPatternMatch(r))
58
55
  };
59
56
  }
57
+ // Clean up report for non-leak errors
58
+ if (fs.existsSync(tmpReport)) {
59
+ fs.unlinkSync(tmpReport);
60
+ }
60
61
  throw new Error(`Gitleaks scan failed: ${e.message}`);
61
62
  }
62
63
  }
@@ -86,7 +87,7 @@ export class GitleaksScanner {
86
87
  throw new Error("Gitleaks not available");
87
88
  }
88
89
  const gitleaksPath = this.binaryManager.getGitleaksPath();
89
- const tmpReport = path.join(os.tmpdir(), `gitleaks-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.json`);
90
+ const tmpReport = path.join(os.tmpdir(), `gitleaks-${Date.now()}-${randomBytes(6).toString("hex")}.json`);
90
91
  try {
91
92
  // Run gitleaks detect on directory
92
93
  await execFileAsync(gitleaksPath, ["detect", "--no-git", "-f", "json", "-r", tmpReport, "-s", dirPath], { timeout: 60000 });
@@ -57,7 +57,18 @@ export class RegexScanner {
57
57
  ".next",
58
58
  "coverage",
59
59
  ".vscode",
60
- ".idea"
60
+ ".idea",
61
+ // Vendored / virtual-env / generated dirs that cause false positives
62
+ "vendor",
63
+ ".venv",
64
+ "venv",
65
+ "__pycache__",
66
+ ".tox",
67
+ ".mypy_cache",
68
+ ".pytest_cache",
69
+ "results",
70
+ ".terraform",
71
+ "bower_components"
61
72
  ];
62
73
  // Merge policy excludePaths into the exclude list
63
74
  if (options?.excludePaths) {
@@ -101,6 +112,10 @@ export class RegexScanner {
101
112
  try {
102
113
  const entries = fs.readdirSync(dir, { withFileTypes: true });
103
114
  for (const entry of entries) {
115
+ // Skip symlinks to prevent traversal outside intended scope
116
+ if (entry.isSymbolicLink()) {
117
+ continue;
118
+ }
104
119
  const fullPath = path.join(dir, entry.name);
105
120
  // Skip excluded directories
106
121
  if (exclude.includes(entry.name)) {
@@ -90,13 +90,13 @@ export const DEFAULT_SECRET_PATTERNS = [
90
90
  // Generic patterns
91
91
  {
92
92
  name: "Generic API Key",
93
- regex: "(?i)(api[_-]?key|apikey)[\\s]*[:=][\\s]*['\"]?[0-9a-zA-Z\\-_]{16,}['\"]?",
93
+ regex: "(?i)(?<![a-zA-Z0-9_])(api[_-]?key|apikey)[\\s]*[:=][\\s]*['\"](?=[0-9a-zA-Z\\-_]*[0-9])[0-9a-zA-Z\\-_]{16,256}['\"]",
94
94
  severity: "high",
95
95
  description: "Generic API key pattern detected"
96
96
  },
97
97
  {
98
98
  name: "Generic Secret",
99
- regex: "(?i)(secret|password|passwd|pwd)[\\s]*[:=][\\s]*['\"]?[0-9a-zA-Z\\-_!@#$%^&*()]{8,}['\"]?",
99
+ regex: "(?i)(?<![a-zA-Z0-9_])(secret|password|passwd|pwd)[\\s]*[:=][\\s]*['\"](?=[^\\s'\"]*[0-9])(?=[^\\s'\"]*[a-zA-Z])[0-9a-zA-Z\\-_!@#$%^&*()]{12,256}['\"]",
100
100
  severity: "high",
101
101
  description: "Generic secret pattern detected"
102
102
  },
@@ -108,21 +108,21 @@ export const DEFAULT_SECRET_PATTERNS = [
108
108
  },
109
109
  {
110
110
  name: "Bearer Token",
111
- regex: "(?i)bearer[\\s]+[a-zA-Z0-9\\-_\\.=]+",
111
+ regex: "(?i)bearer[\\s]+(?=[a-zA-Z0-9\\-_\\.=]*[0-9])(?=[a-zA-Z0-9\\-_\\.=]*[a-zA-Z])[a-zA-Z0-9\\-_\\.=]{20,512}",
112
112
  severity: "high",
113
113
  description: "Bearer token detected"
114
114
  },
115
115
  // Database connection strings
116
116
  {
117
117
  name: "Database Connection String",
118
- regex: "(?i)(postgres|mysql|mongodb)://[^\\s]+:[^\\s]+@[^\\s]+",
118
+ regex: "(?i)(postgres|mysql|mongodb)://[^\\s:@]+:[^\\s@]+@[^\\s]+",
119
119
  severity: "critical",
120
120
  description: "Database connection string with credentials detected"
121
121
  },
122
122
  // JWT
123
123
  {
124
124
  name: "JSON Web Token",
125
- regex: "eyJ[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{10,}",
125
+ regex: "eyJ[A-Za-z0-9_-]{10,2048}\\.[A-Za-z0-9_-]{10,2048}\\.[A-Za-z0-9_-]{10,2048}",
126
126
  severity: "high",
127
127
  description: "JWT token detected"
128
128
  },
@@ -136,7 +136,7 @@ export const DEFAULT_SECRET_PATTERNS = [
136
136
  // PyPI token
137
137
  {
138
138
  name: "PyPI Token",
139
- regex: "pypi-AgEIcHlwaS5vcmc[A-Za-z0-9\\-_]{50,}",
139
+ regex: "pypi-AgEIcHlwaS5vcmc[A-Za-z0-9\\-_]{50,1024}",
140
140
  severity: "critical",
141
141
  description: "PyPI API token detected"
142
142
  }
package/dist/utils/api.js CHANGED
@@ -4,6 +4,24 @@ export const EXIT_SUCCESS = 0;
4
4
  export const EXIT_GENERAL_ERROR = 1;
5
5
  export const EXIT_SCAN_NOT_FOUND = 2;
6
6
  export const EXIT_QUOTA_EXHAUSTED = 3;
7
+ export const EXIT_INSUFFICIENT_SCOPE = 4;
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.
11
+ */
12
+ export function handleScopeError(e) {
13
+ if (!e || e.response?.status !== 403)
14
+ return false;
15
+ const body = e.response?.data;
16
+ const msg = typeof body === "string" ? body : body?.error ?? "";
17
+ if (msg.includes("scope")) {
18
+ 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');
19
+ }
20
+ else {
21
+ console.error(`Error: Forbidden (403) — ${msg || "access denied"}`);
22
+ }
23
+ return true;
24
+ }
7
25
  export function resolveKey(cliKey) {
8
26
  if (cliKey)
9
27
  return cliKey;
@@ -1,6 +1,7 @@
1
1
  import fs from "fs";
2
2
  import os from "os";
3
3
  import path from "path";
4
+ import crypto from "crypto";
4
5
  import https from "https";
5
6
  import { exec, execSync } from "child_process";
6
7
  import { promisify } from "util";
@@ -75,10 +76,10 @@ export class BinaryManager {
75
76
  return false;
76
77
  }
77
78
  try {
78
- const { stdout } = await execAsync(`"${this.getGitleaksPath()}" version`, {
79
- timeout: 5000
80
- });
81
- return stdout.includes("gitleaks version");
79
+ // execAsync rejects on non-zero exit, so reaching here means exit code 0.
80
+ // Accept any successful exit — don't require specific stdout content.
81
+ await execAsync(`"${this.getGitleaksPath()}" version`, { timeout: 5000 });
82
+ return true;
82
83
  }
83
84
  catch {
84
85
  return false;
@@ -91,8 +92,9 @@ export class BinaryManager {
91
92
  const gitleaksPath = binaryPath ?? this.getGitleaksPath();
92
93
  try {
93
94
  const { stdout, stderr } = await execAsync(`"${gitleaksPath}" version`, { timeout: 5000 });
94
- const ok = stdout.includes("gitleaks version");
95
- return { ok, stdout: stdout.trim(), stderr: stderr.trim() };
95
+ // execAsync rejects on non-zero exit, so reaching here means exit code 0.
96
+ // Accept any successful exit — don't require specific stdout content.
97
+ return { ok: true, stdout: stdout.trim(), stderr: stderr.trim() };
96
98
  }
97
99
  catch (e) {
98
100
  const err = e;
@@ -172,6 +174,10 @@ export class BinaryManager {
172
174
  // Log downloaded file size as basic integrity signal
173
175
  const stats = fs.statSync(archivePath);
174
176
  log(` Downloaded: ${(stats.size / 1024).toFixed(1)} KB`);
177
+ // Verify SHA256 checksum against official checksums file
178
+ log("Verifying checksum...");
179
+ await this.verifyChecksum(archivePath, platform, arch, version, log);
180
+ log(" ✓ Checksum verified");
175
181
  // Extract binary
176
182
  log("Extracting binary...");
177
183
  if (platform === "windows") {
@@ -321,6 +327,64 @@ export class BinaryManager {
321
327
  });
322
328
  });
323
329
  }
330
+ /**
331
+ * Verify downloaded archive checksum against official gitleaks checksums file.
332
+ */
333
+ async verifyChecksum(archivePath, platform, arch, version, onProgress) {
334
+ const checksumsUrl = `https://github.com/gitleaks/gitleaks/releases/download/v${version}/gitleaks_${version}_checksums.txt`;
335
+ const checksumsPath = path.join(this.binDir, "checksums.txt");
336
+ try {
337
+ await this.downloadFile(checksumsUrl, checksumsPath, () => { });
338
+ const checksumsContent = fs.readFileSync(checksumsPath, "utf-8");
339
+ const archiveFilename = platform === "windows"
340
+ ? `gitleaks_${version}_windows_${arch}.zip`
341
+ : `gitleaks_${version}_${platform}_${arch}.tar.gz`;
342
+ const expectedHash = this.parseChecksumFile(checksumsContent, archiveFilename);
343
+ if (!expectedHash) {
344
+ throw new Error(`Checksum not found for ${archiveFilename} in checksums file`);
345
+ }
346
+ const actualHash = await this.computeSHA256(archivePath);
347
+ if (actualHash !== expectedHash) {
348
+ throw new Error(`Checksum mismatch for ${archiveFilename}:\n` +
349
+ ` Expected: ${expectedHash}\n` +
350
+ ` Actual: ${actualHash}\n` +
351
+ `The downloaded file may be corrupted or tampered with.`);
352
+ }
353
+ }
354
+ finally {
355
+ if (fs.existsSync(checksumsPath)) {
356
+ fs.unlinkSync(checksumsPath);
357
+ }
358
+ }
359
+ }
360
+ /**
361
+ * Parse a checksums.txt file and return the SHA256 hash for the given filename.
362
+ */
363
+ parseChecksumFile(content, filename) {
364
+ for (const line of content.split("\n")) {
365
+ const trimmed = line.trim();
366
+ if (!trimmed)
367
+ continue;
368
+ // Format: "<sha256> <filename>" (two spaces between hash and filename)
369
+ const parts = trimmed.split(/\s+/);
370
+ if (parts.length >= 2 && parts[1] === filename) {
371
+ return parts[0].toLowerCase();
372
+ }
373
+ }
374
+ return null;
375
+ }
376
+ /**
377
+ * Compute SHA256 hash of a file.
378
+ */
379
+ computeSHA256(filePath) {
380
+ return new Promise((resolve, reject) => {
381
+ const hash = crypto.createHash("sha256");
382
+ const stream = fs.createReadStream(filePath);
383
+ stream.on("data", (data) => hash.update(data));
384
+ stream.on("end", () => resolve(hash.digest("hex")));
385
+ stream.on("error", reject);
386
+ });
387
+ }
324
388
  /**
325
389
  * Extract zip (Windows) — uses PowerShell's Expand-Archive, then copies
326
390
  * only the gitleaks.exe binary to binDir. Cleans up the temp extract dir.
@@ -329,7 +393,10 @@ export class BinaryManager {
329
393
  const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "rafter-gitleaks-"));
330
394
  try {
331
395
  // PowerShell 5+ ships on all supported Windows versions
332
- await execAsync(`powershell -NoProfile -Command "Expand-Archive -Force -LiteralPath '${zipPath}' -DestinationPath '${tempDir}'"`, { timeout: 30000 });
396
+ // Escape single quotes to prevent shell injection ('' is the PS escape for ')
397
+ const safeZipPath = zipPath.replace(/'/g, "''");
398
+ const safeTempDir = tempDir.replace(/'/g, "''");
399
+ await execAsync(`powershell -NoProfile -Command "Expand-Archive -Force -LiteralPath '${safeZipPath}' -DestinationPath '${safeTempDir}'"`, { timeout: 30000 });
333
400
  // Find gitleaks.exe — may be at root or inside a subdirectory
334
401
  const findBinary = (dir) => {
335
402
  for (const entry of fs.readdirSync(dir)) {
@@ -156,15 +156,17 @@ export class SkillManager {
156
156
  async installRafterSkillVerbose(force = false) {
157
157
  const skillPath = this.getRafterSkillPath();
158
158
  const sourcePath = this.getRafterSkillSourcePath();
159
- if (!this.isOpenClawInstalled()) {
160
- return { ok: false, sourcePath, destPath: skillPath, error: `OpenClaw skills directory not found: ${this.getOpenClawSkillsDir()}` };
159
+ // Check if ~/.openclaw exists (the parent dir), not just the skills subdir
160
+ const openclawDir = path.join(os.homedir(), ".openclaw");
161
+ if (!fs.existsSync(openclawDir)) {
162
+ return { ok: false, sourcePath, destPath: skillPath, error: `OpenClaw not found: ${openclawDir}` };
161
163
  }
162
164
  // Check if already installed and not forcing
163
165
  if (!force && this.isRafterSkillInstalled()) {
164
166
  return { ok: true, sourcePath, destPath: skillPath };
165
167
  }
166
168
  try {
167
- // Ensure skills directory exists
169
+ // Ensure skills directory exists (may not exist on fresh OpenClaw installs)
168
170
  const skillsDir = this.getOpenClawSkillsDir();
169
171
  if (!fs.existsSync(skillsDir)) {
170
172
  fs.mkdirSync(skillsDir, { recursive: true });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rafter-security/cli",
3
- "version": "0.5.5",
3
+ "version": "0.6.1",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "rafter": "./dist/index.js"
@@ -20,13 +20,15 @@
20
20
  "license": "MIT",
21
21
  "dependencies": {
22
22
  "@modelcontextprotocol/sdk": "^1.12.0",
23
- "axios": "^1.6.8",
23
+ "axios": "^1.13.5",
24
24
  "chalk": "^5.3.0",
25
+ "chokidar": "^5.0.0",
25
26
  "commander": "^11.1.0",
26
27
  "dotenv": "^16.4.5",
27
28
  "js-yaml": "^4.1.0",
29
+ "minimatch": "^10.2.4",
28
30
  "ora": "^7.0.1",
29
- "tar": "^7.5.7"
31
+ "tar": "^7.5.10"
30
32
  },
31
33
  "devDependencies": {
32
34
  "@types/js-yaml": "^4.0.9",
@@ -27,14 +27,14 @@ fi
27
27
  echo "🔍 Rafter: Scanning staged files for secrets..."
28
28
 
29
29
  # Scan staged files
30
- rafter agent scan --staged --quiet
30
+ rafter scan local --staged --quiet
31
31
 
32
32
  EXIT_CODE=$?
33
33
 
34
34
  if [ $EXIT_CODE -ne 0 ]; then
35
35
  echo -e "${RED}❌ Commit blocked: Secrets detected in staged files${NC}"
36
36
  echo ""
37
- echo " Run: rafter agent scan --staged"
37
+ echo " Run: rafter scan local --staged"
38
38
  echo " To see details and remediate."
39
39
  echo ""
40
40
  echo " To bypass (NOT recommended): git commit --no-verify"
@@ -38,7 +38,7 @@ while read local_ref local_sha remote_ref remote_sha; do
38
38
 
39
39
  echo "🔍 Rafter: Scanning commits being pushed ($local_ref)..."
40
40
 
41
- rafter agent scan --diff "$ref_arg" --quiet
41
+ rafter scan local --diff "$ref_arg" --quiet
42
42
  EXIT_CODE=$?
43
43
 
44
44
  if [ $EXIT_CODE -ne 0 ]; then
@@ -49,7 +49,7 @@ done
49
49
  if [ $FOUND_SECRETS -ne 0 ]; then
50
50
  echo -e "${RED}❌ Push blocked: Secrets detected in commits being pushed${NC}"
51
51
  echo ""
52
- echo " Run: rafter agent scan --diff <remote-sha>"
52
+ echo " Run: rafter scan local --diff <remote-sha>"
53
53
  echo " To see details and remediate."
54
54
  echo ""
55
55
  echo " To bypass (NOT recommended): git push --no-verify"
@@ -6,8 +6,8 @@ openclaw:
6
6
  always: false
7
7
  requires:
8
8
  bins: [rafter]
9
- version: 0.4.0
10
- last_updated: 2026-02-03
9
+ version: 0.5.8
10
+ last_updated: 2026-03-04
11
11
  ---
12
12
 
13
13
  # Rafter Security
@@ -32,7 +32,7 @@ Rafter provides real-time security checks for agent operations:
32
32
  Scan files for secrets before committing.
33
33
 
34
34
  ```bash
35
- rafter agent scan <path>
35
+ rafter scan local <path>
36
36
  ```
37
37
 
38
38
  **When to use:**
@@ -57,21 +57,17 @@ rafter agent scan <path>
57
57
 
58
58
  ### /rafter-bash
59
59
 
60
- Execute shell command with security validation.
60
+ Explicitly run a command through Rafter's security validator.
61
61
 
62
62
  ```bash
63
63
  rafter agent exec <command>
64
64
  ```
65
65
 
66
- **Features:**
67
- - Blocks destructive commands (rm -rf /, fork bombs)
68
- - Requires approval for dangerous operations
69
- - Logs all command attempts
70
- - Scans staged files before git commits
66
+ **When to use:** Only needed in environments where the `PreToolUse` hook is not installed. When `rafter agent init` has been run, all shell commands are validated automatically — you do not need to route commands through this.
71
67
 
72
68
  **Risk levels:**
73
69
  - **Critical** (blocked): rm -rf /, fork bombs, dd to /dev
74
- - **High** (approval required): sudo rm, chmod 777, curl|bash
70
+ - **High** (approval required): sudo rm, chmod 777, curl | bash
75
71
  - **Medium** (approval on moderate+): sudo, chmod, kill -9
76
72
  - **Low** (allowed): npm install, git commit, ls
77
73
 
@@ -269,7 +265,7 @@ Configure with: `rafter agent config set agent.riskLevel moderate`
269
265
 
270
266
  ## Best Practices
271
267
 
272
- 1. **Always scan before commits**: Run `rafter agent scan` before `git commit`
268
+ 1. **Always scan before commits**: Run `rafter scan local` before `git commit`
273
269
  2. **Audit untrusted skills**: Run `/rafter-audit-skill` on skills from unknown sources before installation
274
270
  3. **Review audit logs**: Check `rafter agent audit` after suspicious activity
275
271
  4. **Keep patterns updated**: Patterns updated automatically with CLI updates