Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
8cfd0d4
rc-ksy: migrate scanner engine from gitleaks to betterleaks
Rome-1 May 7, 2026
6aafe84
rc-963: rename gitleaks → betterleaks across user/agent surfaces
Rome-1 May 7, 2026
ec898a9
betterleaks: address review findings (functional + supply-chain harde…
Rome-1 May 7, 2026
a8a82b6
betterleaks: parity polish + test coverage for legacy aliases
Rome-1 May 7, 2026
7c0d64c
betterleaks: hard-cut gitleaks input aliases
Rome-1 May 8, 2026
988af75
betterleaks: address 4-reviewer audit findings
Rome-1 May 8, 2026
a4f5361
betterleaks: PR #93 audit (2 security + simplicity + functionality)
Rome-1 May 8, 2026
c40d7da
ci: auto-publish rafter-security to ClawHub on prod-branch deploy (#94)
Rome-1 May 9, 2026
cbfe354
Merge pull request #93 from Raftersecurity/betterleaks-migration
Raftersecurity May 9, 2026
1b3df17
fix(ci): use 'rafter' as the ClawHub owner handle (not 'raftersecurit…
Rome-1 May 9, 2026
243e9c4
docs(rf-z6sv): bump README pre-commit rev pins to v0.7.9 (#97)
Rome-1 May 9, 2026
2205bfa
feat(rf-hrtd): rafter agent init --dry-run preview without filesystem…
Rome-1 May 9, 2026
941879f
fix(rf-cfjc): action.yml jq parser handles wrapped JSON shape
Rome-1 May 9, 2026
874a86a
rc-dmp: purge user-facing 'scan local' refs; prefer 'rafter secrets'
Rome-1 May 9, 2026
07e1bd8
chore(release): bump to v0.8.0
Rome-1 May 10, 2026
292e0eb
fix(rf-ax2p): e2e test regex for wrapped 'Secrets only' help text
Rome-1 May 10, 2026
1d43dd9
rc-bc9: delete node/.claude/skills/ — eliminate drift from resources/…
Rome-1 May 11, 2026
e656454
test: migrate 'scan local' → 'secrets' in tests + fix comprehensive t…
Rome-1 May 11, 2026
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
4 changes: 2 additions & 2 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ Rafter is a security CLI for AI coding agents. It ships as two feature-identical

- `node/src/commands/` — CLI commands (commander.js)
- `node/src/core/` — Command interceptor, audit logger, config manager
- `node/src/scanners/` — Gitleaks integration + regex-based secret scanner
- `node/src/scanners/` — Betterleaks integration + regex-based secret scanner
- `node/src/commands/agent/init.ts` — Per-platform installation logic (8 platforms)
- `python/rafter_cli/` — Mirrors the Node structure with typer
- `shared-docs/CLI_SPEC.md` — Canonical output contracts and exit codes
Expand All @@ -29,7 +29,7 @@ Rafter is a security CLI for AI coding agents. It ships as two feature-identical
## Key Patterns

- Commands export a `createXCommand()` factory (Node) or use `@app.command()` decorators (Python)
- Scanners use dual-engine: Gitleaks binary first, regex fallback. Patterns defined in `secret-patterns.ts` / `secret_patterns.py`
- Scanners use dual-engine: Betterleaks binary first, regex fallback. Patterns defined in `secret-patterns.ts` / `secret_patterns.py`
- Risk classification: critical > high > medium > low
- Audit log: JSONL format, append-only, documented schema in CLI_SPEC.md
- MCP server: 4 tools + 2 resources over stdio transport
87 changes: 87 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,88 @@ jobs:
TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }}
run: python -m twine upload dist/*

publish-clawhub:
# Publishes the rafter-security skill to ClawHub
# (https://clawhub.ai), the OpenClaw skill registry. The SKILL.md
# frontmatter version (validated by validate-release.yml to match the
# package version) becomes the ClawHub release version.
#
# Skips on forks where the secret isn't configured. On the canonical
# repo, fails loudly on auth or publish errors so a broken token
# surfaces immediately rather than silently skipping releases.
needs: [publish-node]
runs-on: ubuntu-latest
if: github.repository == 'Raftersecurity/rafter-cli'
steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: "20"

- name: Skip if CLAWHUB_TOKEN is not configured
id: gate
env:
CLAWHUB_TOKEN: ${{ secrets.CLAWHUB_TOKEN }}
run: |
if [ -z "$CLAWHUB_TOKEN" ]; then
echo "CLAWHUB_TOKEN secret is not set — skipping ClawHub publish."
echo "skip=true" >> $GITHUB_OUTPUT
else
echo "skip=false" >> $GITHUB_OUTPUT
fi

- name: Stage SKILL.md in a publish directory
if: steps.gate.outputs.skip != 'true'
run: |
# ClawHub expects a directory containing SKILL.md (canonical
# filename). The Node and Python resources are kept in sync by
# validate-release.yml — pick either one as the source of truth.
mkdir -p /tmp/rafter-skill/rafter-security
cp node/resources/rafter-security-skill.md /tmp/rafter-skill/rafter-security/SKILL.md
# Sanity-check: the version in the SKILL.md must match the npm
# publish version. validate-release.yml already enforces this on
# PRs/main, but we re-check here so a release with bypassed
# validation can't ship a stale skill.
SKILL_VERSION=$(sed -n 's/^version: *\(.*\)$/\1/p' /tmp/rafter-skill/rafter-security/SKILL.md | head -1 | tr -d ' ')
PACKAGE_VERSION="${{ needs.publish-node.outputs.version }}"
if [ "$SKILL_VERSION" != "$PACKAGE_VERSION" ]; then
echo "FAIL: SKILL.md version=$SKILL_VERSION but package=$PACKAGE_VERSION"
exit 1
fi
echo "Publishing rafter-security@$SKILL_VERSION to ClawHub"

- name: Authenticate clawhub CLI
if: steps.gate.outputs.skip != 'true'
env:
CLAWHUB_TOKEN: ${{ secrets.CLAWHUB_TOKEN }}
run: |
# Use the token directly via env var rather than a login step —
# `clawhub` reads CLAWHUB_TOKEN automatically when present
# (clawhub login --token persists to disk; we want stateless CI).
npx -y clawhub@latest whoami

- name: Publish to ClawHub
if: steps.gate.outputs.skip != 'true'
env:
CLAWHUB_TOKEN: ${{ secrets.CLAWHUB_TOKEN }}
run: |
# `clawhub skill publish` is idempotent: if the version+content
# fingerprint matches what's already published, it no-ops.
# If the version is unchanged but content differs, the command
# fails (intentional — bumps must come with a version change).
npx -y clawhub@latest skill publish /tmp/rafter-skill/rafter-security \
--version "${{ needs.publish-node.outputs.version }}" \
--owner rafter

- name: Verify the publish landed
if: steps.gate.outputs.skip != 'true'
run: |
# Smoke-check: the published version should be reachable via the
# public registry. Wait a few seconds for index propagation.
sleep 5
npx -y clawhub@latest skill show rafter-security --version "${{ needs.publish-node.outputs.version }}"

create-release:
needs: [publish-node, publish-python]
runs-on: ubuntu-latest
Expand Down Expand Up @@ -181,6 +263,11 @@ jobs:
pip install rafter-cli==${{ needs.publish-python.outputs.version }}
```

**OpenClaw (via ClawHub):**
```bash
clawhub skill install rafter-security
```

See [CHANGELOG.md](https://github.com/raftersecurity/rafter-cli/blob/main/CHANGELOG.md) for details.

smoke-test-node:
Expand Down
14 changes: 12 additions & 2 deletions .github/workflows/test-comprehensive.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,12 @@ jobs:
run: pnpm run build

- name: Verify build
run: node -e "import('./dist/index.js').then(() => console.log('Build OK')).catch(e => { console.error(e); process.exit(1); })"
# Smoke-test that dist/index.js loads and the CLI runs. We can't use
# `node -e "import(...)"` because importing index.js triggers
# `program.parse()` synchronously; with no args Commander prints help
# and exits 1 (Node 20 / Commander 11), so the .then() never runs.
# `--version` is a real Commander action that exits 0 cleanly.
run: node ./dist/index.js --version

- name: Run all tests
run: pnpm test
Expand Down Expand Up @@ -251,7 +256,12 @@ jobs:
pnpm run build

- name: Verify build
run: node -e "import('./dist/index.js').then(() => console.log('Build OK')).catch(e => { console.error(e); process.exit(1); })"
# Smoke-test that dist/index.js loads and the CLI runs. We can't use
# `node -e "import(...)"` because importing index.js triggers
# `program.parse()` synchronously; with no args Commander prints help
# and exits 1 (Node 20 / Commander 11), so the .then() never runs.
# `--version` is a real Commander action that exits 0 cleanly.
run: node ./dist/index.js --version

- name: Run tests
run: pnpm test
21 changes: 21 additions & 0 deletions .github/workflows/validate-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,27 @@ jobs:
fi
echo "Versions match: $NODE_VERSION"

- name: Ensure ClawHub skill version matches package version
# rf-zgwj — the SKILL.md frontmatter version is what ClawHub publishes
# under. Drift here would silently ship a stale version on the next
# `clawhub skill publish` (publish.yml). Both the Node and Python
# resource copies must match the package version exactly.
run: |
PACKAGE_VERSION="${{ steps.node-version.outputs.VERSION }}"
for skill_file in node/resources/rafter-security-skill.md python/rafter_cli/resources/rafter-security-skill.md; do
SKILL_VERSION=$(sed -n 's/^version: *\(.*\)$/\1/p' "$skill_file" | head -1 | tr -d ' ')
if [ -z "$SKILL_VERSION" ]; then
echo "FAIL: $skill_file has no top-level 'version:' field"
exit 1
fi
if [ "$SKILL_VERSION" != "$PACKAGE_VERSION" ]; then
echo "FAIL: $skill_file version=$SKILL_VERSION but package=$PACKAGE_VERSION"
echo "Update the version: line in $skill_file to match."
exit 1
fi
echo "OK: $skill_file version matches ($SKILL_VERSION)"
done

- name: Check CHANGELOG updated
run: |
VERSION="${{ steps.node-version.outputs.VERSION }}"
Expand Down
8 changes: 4 additions & 4 deletions .pre-commit-hooks.yaml
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
- id: rafter-scan
name: Rafter Secret Scanner
description: Scan staged files for secrets (21+ patterns, Gitleaks integration)
entry: rafter scan local --staged --quiet
description: Scan staged files for secrets (21+ patterns, Betterleaks integration)
entry: rafter secrets --staged --quiet
language: system
stages: [pre-commit]
pass_filenames: false

- id: rafter-scan-node
name: Rafter Secret Scan (Node)
description: Scan staged files for hardcoded secrets
entry: rafter scan local --staged --quiet
entry: rafter secrets --staged --quiet
language: node
additional_dependencies: ['@rafter-security/cli']
always_run: true
Expand All @@ -19,7 +19,7 @@
- id: rafter-scan-python
name: Rafter Secret Scan (Python)
description: Scan staged files for hardcoded secrets
entry: rafter scan local --staged --quiet
entry: rafter secrets --staged --quiet
language: python
additional_dependencies: ['rafter-cli']
always_run: true
Expand Down
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.8.0] - 2026-05-10

### Changed
- **Secret-scanning engine migrated from gitleaks to betterleaks** (Node + Python, rc-ksy / rc-963). [Betterleaks](https://github.com/betterleaks/betterleaks) v1.1.2 is the gitleaks successor maintained by the same authors. JSON report shape is unchanged; what changed is the binary, the CLI subcommand (`detect --no-git -s` → `dir <path>`), the release URL, and the checksum filename.
- **Breaking:** the legacy CLI surface has been removed entirely. `--with-gitleaks`, `--engine gitleaks`, and `rafter agent update-gitleaks` now error out (unknown option / invalid engine / unknown command). Use `--with-betterleaks`, `--engine betterleaks`, and `rafter agent update-betterleaks`. **This is the reason for the 0.8.0 minor bump on a 0.x line.**
- **Soft landing for existing installs:** `rafter agent verify` and `rafter agent status` continue to detect a leftover `~/.rafter/bin/gitleaks` (or `gitleaks` on PATH) and emit "legacy gitleaks at X — run: rafter agent update-betterleaks" instead of a confusing "not found". Verify exits 0 in this case (was a hard fail before this fix).
- **Supply-chain hardening:** SHA256 hashes for the bundled `BETTERLEAKS_VERSION` are pinned in source, so the default install no longer trusts the release-page `checksums.txt` to authenticate itself. Tar/zip extraction now rejects symlink/hardlink/device entries (mitigates a malicious-release symlink-redirect that the subsequent `chmod +x` would have followed). Downloads refuse non-https URLs. The optional `--version` flag is validated against `^[A-Za-z0-9._-]+$` to neutralize URL injection. Targets passed to betterleaks are preceded by `--` so a path beginning with `-` isn't parsed as a flag.
- Internal renames: `GitleaksScanner` → `BetterleaksScanner`, `*_gitleaks` methods → `*_betterleaks`, `GITLEAKS_VERSION` → `BETTERLEAKS_VERSION`. New tests cover pinned-hash table completeness, `--version` validation, non-https refusal, and the alias-removal contract.
- **Purge user-facing `scan local` references** (rc-dmp). The Commander/Typer subcommand was already hidden behind `rafter secrets` — this pass mops up the surfaces that still recommended the alias: `.pre-commit-hooks.yaml` (3 hook entries), `fixtures/vulnerable-repo/README.md` demo commands, `node/.claude/skills/*` dev copies (9 refs), `node/src/commands/issues/from-scan.ts` `--from-local` help text, and `shared-docs/CLI_SPEC.md` baseline example. Alias plumbing, internal comments, and the alias-test path are intentionally retained for backward compat.

### Added
- **`rafter agent init --dry-run`** (Node + Python, rf-hrtd). Prints every file path the command would create, modify, or download — without making any changes. Lists the always-written `~/.rafter/config.json` and bin/patterns dirs, then per-enabled-platform sections (Claude Code, Codex, Gemini, Cursor, Windsurf, Continue.dev, Aider, OpenClaw) with file paths and short notes about what each write contains. Optional Betterleaks binary download is listed as `DOWNLOAD`. The plan is built from the same resolved `want_*` / `has_*` booleans the install path uses, so the listing mirrors what would actually run. Three new Node tests + three new Python tests confirm `--dry-run` writes nothing (not even the always-create `~/.rafter/config.json`) and lists every section under `--local --all`. Closes the rf-v85b P0-1 review concern: security-conscious adopters can preview every edit before accepting.
- **ClawHub auto-publish on release** (CI). `.github/workflows/publish.yml` now runs `clawhub skill publish` against the rafter-security SKILL.md after every `prod`-branch deploy. Skips on forks (gated on `secrets.CLAWHUB_TOKEN`); fails loudly on auth or publish errors on the canonical repo. `validate-release.yml` was extended to enforce that `version:` in both Node and Python copies of `rafter-security-skill.md` matches the package version — drift would silently ship a stale ClawHub release. OpenClaw users can now install rafter via `clawhub skill install rafter-security` as an alternative to `rafter agent init --with-openclaw`.

### Fixed
- **GitHub Action `finding-count` always 0** (rf-cfjc). The composite action's jq count query (`[.[].matches[]] | length`) errored on the wrapped JSON shape introduced in v0.7.7 (rf-0pch: `{_note, scan_mode, triage_applied, results: [...]}`) and silently fell through to `"0"`. Test Composite Action's `detect secrets in fixture` job had been failing on every push since betterleaks merged, even though the scanner was correctly detecting the AKIA fixture. Replaced with a type-aware query that handles both the wrapped object (current) and the bare array (older `version:` pins).
- **CI ClawHub publish handle** (#96). The actual ClawHub owner handle is `rafter`, not `raftersecurity`. Without this, the first real ClawHub publish would have failed with "owner not found".
- **README pre-commit rev pins** (rf-z6sv, #97). Both pre-commit examples in README.md were stuck at v0.7.1; bumped to track the latest published tag so new adopters get the rf-zfhj GitHub Action fix, the rf-zgwj OpenClaw ClawHub-shape fix, and the audit-log hash-chain hardening.

## [0.7.9] - 2026-05-08

### Fixed
Expand Down
6 changes: 3 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,15 @@ cd python && poetry install && pytest
│ │ │ ├── audit-logger.ts # JSONL audit trail
│ │ │ └── config-manager.ts # .rafter.yml + global config
│ │ └── scanners/
│ │ ├── gitleaks.ts # Gitleaks binary integration
│ │ ├── betterleaks.ts # Betterleaks binary integration
│ │ ├── secret-patterns.ts # DEFAULT_SECRET_PATTERNS array (21+ patterns)
│ │ └── regex-scanner.ts # RegexScanner class (imports secret-patterns)
│ └── tests/ # Vitest test files
├── python/ # Python implementation (rafter-cli on PyPI)
│ ├── rafter_cli/
│ │ ├── commands/ # CLI commands (typer)
│ │ ├── core/ # Mirrors node/src/core/
│ │ └── scanners/ # secret_patterns.py + regex_scanner.py + gitleaks.py
│ │ └── scanners/ # secret_patterns.py + regex_scanner.py + betterleaks.py
│ └── tests/ # pytest test files
├── shared-docs/ # Canonical specs (both implementations follow these)
│ └── CLI_SPEC.md # Output contracts, exit codes, JSON schemas
Expand All @@ -58,7 +58,7 @@ cd python && poetry install && pytest

**Risk classification**: Commands are classified into 4 tiers (critical/high/medium/low) by pattern matching in `command-interceptor.ts`. Policy files (`.rafter.yml`) can override defaults.

**Secret scanning**: Dual-engine — tries Gitleaks binary first (higher accuracy), falls back to built-in regex patterns (21+ patterns, zero dependencies). Deterministic for a given version.
**Secret scanning**: Dual-engine — tries Betterleaks binary first (higher accuracy), falls back to built-in regex patterns (21+ patterns, zero dependencies). Deterministic for a given version. Betterleaks is the gitleaks successor maintained by the original gitleaks authors. Existing installs with a leftover `~/.rafter/bin/gitleaks` are detected by `agent verify`/`status` so users get an upgrade hint, but the legacy CLI flags (`--with-gitleaks`, `--engine gitleaks`, `update-gitleaks`) have been removed.

**MCP server**: `rafter mcp serve` exposes 4 tools (`scan_secrets`, `evaluate_command`, `read_audit_log`, `get_config`) and 2 resources (`rafter://config`, `rafter://policy`) over stdio.

Expand Down
11 changes: 11 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,17 @@ poetry install
pytest # 400+ tests, ~2s
```

### (Optional) Dogfood the skills locally

If you want Claude Code to auto-load the rafter skills while working *in* this repo, install them from the local build into `node/.claude/skills/` (gitignored):

```bash
cd node && pnpm run build
node dist/index.js agent init --with-claude-code --local
```

This is the same install path users run. The previous checked-in dev copies were removed because they drifted from `node/resources/skills/` (the shipped source of truth).

## Dual Implementation

Rafter ships as both `@rafter-security/cli` (npm) and `rafter-cli` (PyPI) with full feature parity. **Every change must be implemented in both Node.js and Python.**
Expand Down
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ FROM node:22-alpine

RUN apk add --no-cache git \
&& npm install -g @rafter-security/cli \
&& rafter agent init --with-gitleaks 2>/dev/null || true
&& rafter agent init --with-betterleaks

WORKDIR /workspace

Expand Down
Loading
Loading