Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.9.0] - 2026-07-08

### Added

- **Native OpenCode support** (sable-l8e5). `rafter agent init --with-opencode` registers the Rafter MCP server in OpenCode's config (`~/.config/opencode/opencode.json`, `mcp` block with `type: "local"`), and OpenCode is auto-detected on init. First-class platform support now spans 10 agents. Node + Python parity, with a `recipes/opencode.md` guide.
- **Multi-provider remote scan** (sable-w79q). `rafter run` now infers the git remote's provider — GitLab, Gitea (`codeberg.org` / `*.gitea.io`), and Bitbucket, in addition to GitHub — and sends an additive `provider` + `repo_url` in the scan request. New optional `--provider` / `--repo-url` overrides. **Fully backward-compatible:** GitHub scans send a byte-identical request (no new fields). Non-GitHub scanning additionally depends on backend rollout.
- **DigitalOcean Personal Access Token secret pattern** (#26, #189). New `dop_v1_[a-f0-9]{64}` detection pattern (severity `critical`) added to the built-in regex scanner in both Node and Python, with tests in each suite. Thanks to @Minh-Nguyen-2k7 for the contribution.

## [0.8.10] - 2026-06-28

### Changed
Expand Down
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
<a href="#supported-platforms"><img alt="Continue.dev supported" src="https://img.shields.io/badge/Continue.dev-supported-7c3aed?style=flat&labelColor=1e1b2e"></a>
<a href="#supported-platforms"><img alt="Aider supported" src="https://img.shields.io/badge/Aider-supported-10b981?style=flat&labelColor=111827"></a>
<a href="#supported-platforms"><img alt="Hermes supported" src="https://img.shields.io/badge/Hermes-supported-9333ea?style=flat&labelColor=1a0a2e"></a>
<a href="#supported-platforms"><img alt="OpenCode supported" src="https://img.shields.io/badge/OpenCode-supported-fab283?style=flat&labelColor=0a0a0a"></a>
</p>

Multi-language CLI for [Rafter](https://rafter.so) — the security toolkit built for AI coding agents and the developers who use them.
Expand All @@ -25,7 +26,7 @@ Rafter is a **security primitive** that any developer or agent can call and trus

**Two capabilities in one package:**

1. **Local Security Toolkit** (free, no account) — Fast secret scanning (21+ built-in patterns, deterministic for a given version), policy enforcement with risk-tiered rules, pre-commit hooks, extension auditing, custom rule authoring, and full audit logging. Works offline. **No API key. No telemetry. No data leaves your machine.** Supports Claude Code, Codex CLI, OpenClaw, Gemini CLI, Cursor, Windsurf, Continue.dev, Aider, and Hermes.
1. **Local Security Toolkit** (free, no account) — Fast secret scanning (21+ built-in patterns, deterministic for a given version), policy enforcement with risk-tiered rules, pre-commit hooks, extension auditing, custom rule authoring, and full audit logging. Works offline. **No API key. No telemetry. No data leaves your machine.** Supports Claude Code, Codex CLI, OpenClaw, Gemini CLI, Cursor, Windsurf, Continue.dev, Aider, Hermes, and OpenCode.

2. **Remote Code Analysis** — Deep security audits that combine agentic analysis with a full SAST/SCA toolchain. Rafter's engine examines your codebase the way a professional penetration tester would — tracing data flows, reasoning about business logic, and surfacing vulnerabilities that static rules alone miss — then cross-references findings with industry-standard SAST, SCA, and secret-detection tools. Structured reports in JSON or Markdown. Pipe to any tool, feed to any workflow.

Expand Down Expand Up @@ -183,7 +184,7 @@ rafter agent disable gemini # opt a single platform out

This command:
- Creates `~/.rafter/` config and audit log (or `./.rafter/` with `--local` for ephemeral / containerized / benchmark setups)
- Auto-detects Claude Code, Codex CLI, OpenClaw, Gemini, Cursor, Windsurf, Continue.dev, Aider, and Hermes
- Auto-detects Claude Code, Codex CLI, OpenClaw, Gemini, Cursor, Windsurf, Continue.dev, Aider, Hermes, and OpenCode
- With `--with-*` or `--all`: installs Rafter skills/extensions to opted-in agents
- With `--with-betterleaks` or `--all`: downloads [Betterleaks](https://github.com/betterleaks/betterleaks) (the gitleaks successor maintained by the original gitleaks authors) for enhanced secret scanning. Falls back to built-in 21-pattern regex scanner.

Expand Down Expand Up @@ -484,6 +485,7 @@ Add to any MCP client config:
| Continue.dev | MCP server | `~/.continue` | `~/.continue/config.json` |
| Aider | MCP server | `~/.aider.conf.yml` | `~/.aider.conf.yml` |
| Hermes | MCP server | `~/.hermes` | `~/.hermes/config.yaml` |
| OpenCode | MCP server | `~/.config/opencode` | `~/.config/opencode/opencode.json` |

`rafter agent init` auto-detects which platforms are installed. Use `--with-*` flags or `--all` to install integrations.

Expand All @@ -496,7 +498,7 @@ Add to any MCP client config:

Install, remove, or audit them at any time with `rafter skill list/install/uninstall/review`.

**MCP-based platforms** (Gemini, Cursor, Windsurf, Continue.dev, Aider, Hermes) connect to the Rafter MCP server (`rafter mcp serve`), which exposes `scan_secrets`, `evaluate_command`, `read_audit_log`, and `get_config` tools. See individual setup recipes in [`recipes/`](recipes/).
**MCP-based platforms** (Gemini, Cursor, Windsurf, Continue.dev, Aider, Hermes, OpenCode) connect to the Rafter MCP server (`rafter mcp serve`), which exposes `scan_secrets`, `evaluate_command`, `read_audit_log`, and `get_config` tools. See individual setup recipes in [`recipes/`](recipes/).

---

Expand Down
2 changes: 1 addition & 1 deletion node/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@rafter-security/cli",
"version": "0.8.10",
"version": "0.9.0",
"type": "module",
"repository": {
"type": "git",
Expand Down
2 changes: 1 addition & 1 deletion node/resources/rafter-security-skill.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
name: rafter-security
description: Security toolkit for AI workflows. Use when scanning code or repos for vulnerabilities, auditing third-party skills/MCPs/agent configs before installing, evaluating shell commands before running them, or generating secure design questions for new features. Provides `rafter run` (remote SAST + SCA, needs RAFTER_API_KEY), `rafter secrets` (offline secrets-only), `rafter agent exec --dry-run` (command-risk classification), and `rafter skill review`.
version: 0.8.10
version: 0.9.0
homepage: https://rafter.so
metadata:
openclaw:
Expand Down
48 changes: 48 additions & 0 deletions node/src/commands/agent/components.ts
Original file line number Diff line number Diff line change
Expand Up @@ -938,6 +938,53 @@ function hermesMcp(): ComponentSpec {
};
}

/**
* OpenCode MCP server entry (~/.config/opencode/opencode.json).
*
* OpenCode's schema differs from Cursor/Windsurf: the block is `mcp` (not
* `mcpServers`), each local server carries `type: "local"`, and command + args
* are a single `command` array. Verified against
* https://opencode.ai/docs/mcp-servers/ (sable-l8e5).
*/
function openCodeMcp(): ComponentSpec {
const home = os.homedir();
const configPath = path.join(home, ".config", "opencode", "opencode.json");
const entry = {
type: "local" as const,
command: [RAFTER_MCP_ENTRY.command, ...RAFTER_MCP_ENTRY.args],
enabled: true,
};
return {
id: "opencode.mcp",
platform: "opencode",
kind: "mcp",
description: "OpenCode MCP server entry (~/.config/opencode/opencode.json)",
detectDir: path.join(home, ".config", "opencode"),
path: configPath,
isInstalled: () => {
if (!fs.existsSync(configPath)) return false;
const cfg = readJson(configPath);
return !!cfg.mcp?.rafter;
},
install: () => {
const dir = path.join(home, ".config", "opencode");
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
let cfg: Record<string, any> = fs.existsSync(configPath) ? readJson(configPath) : {};
// Guard against valid-but-non-object top-level JSON (array/string/number).
if (!cfg || typeof cfg !== "object" || Array.isArray(cfg)) cfg = {};
if (!cfg.$schema) cfg.$schema = "https://opencode.ai/config.json";
if (!cfg.mcp || typeof cfg.mcp !== "object" || Array.isArray(cfg.mcp)) cfg.mcp = {};
cfg.mcp.rafter = { ...entry };
writeJson(configPath, cfg);
},
uninstall: () => {
if (!fs.existsSync(configPath)) return;
const cfg = readJson(configPath);
if (removeKey(cfg.mcp, "rafter")) writeJson(configPath, cfg);
},
};
}

/**
* Aider read-only context: writes RAFTER.md and adds it to .aider.conf.yml `read:`.
*
Expand Down Expand Up @@ -1079,6 +1126,7 @@ export function getComponentRegistry(): ComponentSpec[] {
continueMcp(),
aiderRead(),
hermesMcp(),
openCodeMcp(),
openclawSkill(),
];
}
Expand Down
95 changes: 94 additions & 1 deletion node/src/commands/agent/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ function printDryRunPlan(plan: {
wantContinue: boolean;
wantAider: boolean;
wantHermes: boolean;
wantOpenCode: boolean;
wantBetterleaks: boolean;
wantSkillScanner: boolean;
riskLevel: string;
Expand Down Expand Up @@ -176,6 +177,12 @@ function printDryRunPlan(plan: {
W(path.join(plan.root, ".hermes", "config.yaml"), "mcp_servers.rafter entry merged into existing YAML");
}

if (plan.wantOpenCode) {
console.log();
console.log("OpenCode (--with-opencode):");
W(path.join(plan.root, ".config", "opencode", "opencode.json"), "mcp.rafter local/stdio entry merged into existing JSON");
}

if (plan.wantOpenClaw) {
console.log();
console.log("OpenClaw (--with-openclaw):");
Expand Down Expand Up @@ -894,6 +901,63 @@ function installHermesMcp(root: string): boolean {
return true;
}

/**
* OpenCode MCP server entry. OpenCode's schema differs from Cursor/Windsurf:
* the block is `mcp` (not `mcpServers`), each local server has `type: "local"`,
* and the command + args are a single `command` array (not split). Verified
* against https://opencode.ai/docs/mcp-servers/ (rf-opencode).
*/
const RAFTER_OPENCODE_MCP_ENTRY = {
type: "local" as const,
command: [RAFTER_MCP_ENTRY.command, ...RAFTER_MCP_ENTRY.args],
enabled: true,
};

/**
* Install MCP server config for OpenCode (~/.config/opencode/opencode.json).
*
* OpenCode reads a global config at ~/.config/opencode/opencode.json and a
* project-level opencode.json (project takes precedence). We register the
* local stdio server `rafter mcp serve` under the `mcp` block. The `$schema`
* pointer is seeded on first write so editors get completion. Any existing
* keys / other MCP servers are preserved.
*/
function installOpenCodeMcp(root: string): boolean {
const openCodeDir = path.join(root, ".config", "opencode");
const configPath = path.join(openCodeDir, "opencode.json");

if (!fs.existsSync(openCodeDir)) {
fs.mkdirSync(openCodeDir, { recursive: true });
}

let config: Record<string, any> = {};
if (fs.existsSync(configPath)) {
try {
const parsed = JSON.parse(fs.readFileSync(configPath, "utf-8"));
// Guard against valid-but-non-object top-level JSON (array/string/number).
// Mirrors the Python `isinstance(loaded, dict)` check so a wrong-shaped
// file is replaced rather than silently mangled by property assignment.
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
config = parsed;
} else {
console.log(fmt.warning("Existing OpenCode opencode.json was not a JSON object, creating new one"));
}
} catch {
console.log(fmt.warning("Existing OpenCode opencode.json was unreadable, creating new one"));
}
}

if (!config.$schema) config.$schema = "https://opencode.ai/config.json";
if (!config.mcp || typeof config.mcp !== "object" || Array.isArray(config.mcp)) {
config.mcp = {};
}
config.mcp.rafter = { ...RAFTER_OPENCODE_MCP_ENTRY };

fs.writeFileSync(configPath, JSON.stringify(config, null, 2), "utf-8");
console.log(fmt.success(`Installed Rafter MCP server to ${configPath}`));
return true;
}

function installAiderRead(root: string): boolean {
const rafterMdPath = path.join(root, "RAFTER.md");
const configPath = path.join(root, ".aider.conf.yml");
Expand Down Expand Up @@ -1087,6 +1151,7 @@ export function createInitCommand(): Command {
.option("--with-windsurf", "Install Windsurf integration")
.option("--with-continue", "Install Continue.dev integration")
.option("--with-hermes", "Install Hermes integration")
.option("--with-opencode", "Install OpenCode integration")
.option("--with-betterleaks", "Download and install Betterleaks binary")
.option("--with-skill-scanner", "Install the optional skill-scanner deep engine (heavy; audit-skill --deep)")
.option("--all", "Install all detected integrations and download Betterleaks")
Expand Down Expand Up @@ -1128,6 +1193,7 @@ export function createInitCommand(): Command {
const hasContinueDev = scope === "user" && fs.existsSync(path.join(os.homedir(), ".continue"));
const hasAider = scope === "user" && fs.existsSync(path.join(os.homedir(), ".aider.conf.yml"));
const hasHermes = scope === "user" && fs.existsSync(path.join(os.homedir(), ".hermes"));
const hasOpenCode = scope === "user" && fs.existsSync(path.join(os.homedir(), ".config", "opencode"));

// Resolve opt-in flags (--all enables all detected, --interactive prompts).
// In --local scope, --all is restricted to platforms that have a project-local
Expand Down Expand Up @@ -1155,6 +1221,12 @@ export function createInitCommand(): Command {
// reads ~/.hermes/config.yaml; the project-local install story isn't
// established. Excluded from --all in --local for the same reason.
let wantHermes = opts.withHermes || (opts.all && !opts.local);
// OpenCode: MCP-based, user scope only. OpenCode reads a global config at
// ~/.config/opencode/opencode.json; a project-local install story via
// --local isn't wired here, so (like Hermes) it's excluded from --all in
// --local scope. It supports MCP local/stdio servers and AGENTS.md
// natively (https://opencode.ai/docs/mcp-servers/, sable-l8e5).
let wantOpenCode = opts.withOpencode || (opts.all && !opts.local);
let wantBetterleaks = opts.withBetterleaks || (opts.all && !opts.local);
// skill-scanner is heavy and opt-in only — deliberately NOT folded into --all.
const wantSkillScanner = !!opts.withSkillScanner;
Expand All @@ -1173,6 +1245,7 @@ export function createInitCommand(): Command {
if (hasContinueDev && !wantContinue) wantContinue = await askYesNo("Install Continue.dev MCP server?");
if (hasAider && !wantAider) wantAider = await askYesNo("Install Aider MCP server?");
if (hasHermes && !wantHermes) wantHermes = await askYesNo("Install Hermes MCP server?");
if (hasOpenCode && !wantOpenCode) wantOpenCode = await askYesNo("Install OpenCode MCP server?");
if (!wantBetterleaks) wantBetterleaks = await askYesNo("Download Betterleaks binary (enhanced scanning)?");
console.log();
}
Expand All @@ -1188,6 +1261,7 @@ export function createInitCommand(): Command {
if (hasContinueDev) detected.push("Continue.dev");
if (hasAider) detected.push("Aider");
if (hasHermes) detected.push("Hermes");
if (hasOpenCode) detected.push("OpenCode");

if (detected.length > 0) {
console.log(fmt.info(`Detected environments: ${detected.join(", ")}`));
Expand All @@ -1207,6 +1281,7 @@ export function createInitCommand(): Command {
if (wantContinue && !hasContinueDev) console.log(fmt.warning("Continue.dev requested but not detected (~/.continue not found)"));
if (wantAider && !hasAider) console.log(fmt.warning("Aider requested but not detected (~/.aider.conf.yml not found)"));
if (wantHermes && !hasHermes) console.log(fmt.warning("Hermes requested but not detected (~/.hermes not found)"));
if (wantOpenCode && !hasOpenCode) console.log(fmt.warning("OpenCode requested but not detected (~/.config/opencode not found)"));
}

// --dry-run: print every file path the command would touch, then
Expand All @@ -1226,6 +1301,7 @@ export function createInitCommand(): Command {
wantContinue: wantContinue && (hasContinueDev || opts.local),
wantAider: wantAider && (hasAider || opts.local),
wantHermes: wantHermes && hasHermes,
wantOpenCode: wantOpenCode && hasOpenCode,
wantBetterleaks,
wantSkillScanner,
riskLevel: opts.riskLevel,
Expand Down Expand Up @@ -1517,6 +1593,20 @@ export function createInitCommand(): Command {
}
}

// Install OpenCode integration if opted in (sable-l8e5).
// User scope only — OpenCode reads a global config at
// ~/.config/opencode/opencode.json. MCP-based: we register the local
// stdio server `rafter mcp serve` under the `mcp` block.
let openCodeOk = false;
if (wantOpenCode && hasOpenCode) {
try {
openCodeOk = installOpenCodeMcp(root);
if (openCodeOk) manager.set("agent.environments.opencode.enabled", true);
} catch (e) {
console.error(fmt.error(`Failed to install OpenCode integration: ${e}`));
}
}

// Install global instruction files for platforms that support them.
// Cursor is intentionally absent — Cursor uses per-skill rules + the
// rafter sub-agent installed in the Cursor branch above (rf-svn3).
Expand All @@ -1531,7 +1621,7 @@ export function createInitCommand(): Command {
console.log(fmt.success("Agent security initialized!"));
console.log();

const anyIntegration = openclawOk || claudeCodeOk || codexOk || geminiOk || cursorOk || windsurfOk || continueOk || aiderOk;
const anyIntegration = openclawOk || claudeCodeOk || codexOk || geminiOk || cursorOk || windsurfOk || continueOk || aiderOk || hermesOk || openCodeOk;

if (anyIntegration) {
console.log("Next steps:");
Expand All @@ -1543,6 +1633,8 @@ export function createInitCommand(): Command {
if (windsurfOk) console.log(" - Restart Windsurf to load MCP server");
if (continueOk) console.log(" - Restart Continue.dev to load MCP server");
if (aiderOk) console.log(" - Restart Aider to load RAFTER.md from .aider.conf.yml read:");
if (hermesOk) console.log(" - Restart Hermes to load MCP server");
if (openCodeOk) console.log(" - Restart OpenCode to load MCP server");
} else if (scope === "project") {
console.log("No integrations were installed. In --local mode, pass one or more opt-in flags:");
console.log(" rafter agent init --local --with-claude-code");
Expand All @@ -1561,6 +1653,7 @@ export function createInitCommand(): Command {
if (hasContinueDev) console.log(" rafter agent init --with-continue # Continue.dev only");
if (hasAider) console.log(" rafter agent init --with-aider # Aider only");
if (hasHermes) console.log(" rafter agent init --with-hermes # Hermes only");
if (hasOpenCode) console.log(" rafter agent init --with-opencode # OpenCode only");
} else {
console.log("No agent environments detected. Install an agent tool and re-run with --with-<tool>.");
}
Expand Down
2 changes: 2 additions & 0 deletions node/src/commands/agent/status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ export function createStatusCommand(): Command {
{ name: "Windsurf", flag: "--with-windsurf", configDir: path.join(home, ".codeium", "windsurf"), configFile: path.join(home, ".codeium", "windsurf", "mcp_config.json"), needle: "rafter" },
{ name: "Continue.dev", flag: "--with-continue", configDir: path.join(home, ".continue"), configFile: path.join(home, ".continue", "config.json"), needle: "rafter" },
{ name: "Hermes", flag: "--with-hermes", configDir: path.join(home, ".hermes"), configFile: path.join(home, ".hermes", "config.yaml"), needle: "rafter" },
{ name: "OpenCode", flag: "--with-opencode", configDir: path.join(home, ".config", "opencode"), configFile: path.join(home, ".config", "opencode", "opencode.json"), needle: "rafter" },
];

for (const agent of mcpAgents) {
Expand Down Expand Up @@ -267,6 +268,7 @@ function detectAgents(home: string): string[] {
["continue", path.join(home, ".continue")],
["aider", path.join(home, ".aider.conf.yml")],
["hermes", path.join(home, ".hermes")],
["opencode", path.join(home, ".config", "opencode")],
];
return candidates
.filter(([, p]) => fs.existsSync(p))
Expand Down
Loading
Loading