A configuration hygiene checker for your AI development environment.
One command. 28 checks — 13 scored plus 15 advisory. A hygiene score out of 100. Know where you stand before something breaks.
Scope. rigscore measures configuration hygiene, not runtime security. It inspects the files on disk — governance docs, MCP configs, Docker settings, skill files, permissions — and scores what those files say. It does not observe the running agent, intercept tool calls, or hash live MCP tool descriptions. Pair it with a runtime scanner and source-level SAST; it does not replace them. See known limits for what rigscore does not catch.
npx github:Back-Road-Creative/rigscore ╭────────────────────────────────────────╮
│ │
│ rigscore v2.1.0 │
│ AI Dev Environment Hygiene Check │
│ │
╰────────────────────────────────────────╯
Scanning /home/user/my-project ...
✓ CLAUDE.md governance.......... [pattern] 10/10
✓ Claude settings safety........ [mechanical] 8/8
✓ Cross-config coherence........ [keyword] 14/14
✓ Credential storage hygiene.... [mechanical] 6/6
↷ Deep source secrets........... [pattern] N/A
✓ Docker security............... [mechanical] 6/6
✓ Secret exposure............... [mechanical] 8/8
✓ Git hooks..................... [mechanical] 2/2
✓ Infrastructure security....... [mechanical] 5/6
⚠ Instruction effectiveness..... [keyword] advisory
✗ MCP server configuration...... [mechanical] 0/14
✓ Permissions hygiene........... [mechanical] 4/4
↷ Site security................. [mechanical] N/A
✓ Skill file safety............. [pattern] 10/10
✓ Unicode steganography......... [pattern] 4/4
↷ Windows/WSL security.......... [mechanical] N/A
✓ Skill ↔ governance coherence.. [keyword] advisory
✓ Workflow maturity............. [keyword] advisory
↷ Network exposure.............. [mechanical] N/A
╭────────────────────────────────────────╮
│ │
│ HYGIENE SCORE: 78/100 │
│ Grade: B │
│ Posture: Standard │
│ Practice: 82/100 (B) │
│ │
╰────────────────────────────────────────╯
mechanical = deterministic config check · pattern = regex/structural · keyword = presence detection
CRITICAL (1)
✗ MCP server "filesystem" has broad filesystem access: /
→ Scope filesystem access to your project directory only.
rigscore scores AI-agent config hygiene and catches contradictions between what your governance file claims and what your actual configuration does — in one local command, no account, no API token, passable as a CI gate with a single --fail-under threshold.
Most AI-agent security scanning today is either finding-stream static analysis or manual review. rigscore fills a narrower slot: a single hygiene score with an A–F grade, a cross-config coherence pass that compares governance claims to observed behavior (MCP scope, Docker privileges, approval gates), and a CI-gate exit code. It runs fully offline by default; --online is opt-in for site probes and MCP supply-chain verification.
It's the thing you run before you stand up a SARIF pipeline or adopt an enterprise scanner — though it also ships offline MCP rug-pull pinning (mcp-hash/mcp-pin/mcp-verify), an in-scan CycloneDX 1.6 AI-BOM (--cyclonedx) covering the MCP grant surface, and a two-axis Security + Practice score most scanners don't. If your CLAUDE.md says "never access /etc" and your MCP config mounts /, rigscore tells you.
What rigscore checks: MCP scope and supply chain, cross-config coherence, skill-file injection vectors, governance quality, Claude settings bypass combos, secret exposure in config files, container and devcontainer isolation, Unicode steganography, git hooks, file permissions, credential storage, and (advisory) instruction effectiveness, skill↔governance coherence, workflow maturity, Windows/WSL boundary, site security, network exposure, agent output schemas, documentation coverage, loop governance, spec goals, CI agent caps, agent memory hygiene, AI-use disclosure, and sandbox posture.
Run it. See the score. Fix what's broken.
Every check in rigscore's output carries an enforcement-grade label so you can see how each point was earned, not just how many. The label is display-only — it does not affect scoring.
[mechanical]— deterministic config inspection. Parses JSON/YAML/file state and compares to known-bad constants, file modes, or structural invariants. Cannot be gamed by wording. 15 of 28 checks.[pattern]— regex or structural scan against file content. Secret signatures, Unicode codepoint classes, markdown structural patterns. Resistant to simple wording tricks but evadable by novel obfuscation. 7 of 28 checks.[keyword]— presence of words or phrases in governance prose. These checks pass if the right words appear, which means a CLAUDE.md can keyword-stuff its way to green while substantively reversing intent. 6 of 28 checks — seeTHREAT-MODEL.mdfor the gameability gap andtest/keyword-gaming.test.jsfor concrete bypasses.
Reproduce the grade breakdown (grades are static per check, so this is config-independent):
node bin/rigscore.js --json . | jq -r '.results|group_by(.enforcementGrade)[]|"\(.[0].enforcementGrade): \(length)"'The 13-scored / 15-advisory split comes from the default weights in src/constants.js. Scan a directory with no .rigscorerc.json to see it un-skewed — rigscore's own rc file disables four checks, which zeroes their weight in a self-scan:
node bin/rigscore.js --json "$(mktemp -d)" \
| jq '{total:(.results|length), scored:([.results[]|select(.weight>0)]|length), advisory:([.results[]|select(.weight==0)]|length)}'No setup. No accounts. No data leaves your machine.
# Run on the current directory
npx github:Back-Road-Creative/rigscore
# Run on a specific project
npx github:Back-Road-Creative/rigscore /path/to/project
# Output as JSON (for CI integration)
npx github:Back-Road-Creative/rigscore --json
# SARIF output (for GitHub Advanced Security)
npx github:Back-Road-Creative/rigscore --sarif
# CI mode (SARIF + no color + no CTA)
npx github:Back-Road-Creative/rigscore --ci --fail-under 80
# Generate a README badge
npx github:Back-Road-Creative/rigscore --badge
# Scan a monorepo (recursive mode)
npx github:Back-Road-Creative/rigscore . --recursive --depth 2
# Run a single check
npx github:Back-Road-Creative/rigscore --check docker-security
# Deep source secret scanning
npx github:Back-Road-Creative/rigscore --deep
# Online checks (site-security, MCP supply-chain verification)
npx github:Back-Road-Creative/rigscore --online
# Force refresh of the MCP registry cache (implies --online)
npx github:Back-Road-Creative/rigscore --refresh-mcp-registry
# Auto-fix safe issues (dry run)
npx github:Back-Road-Creative/rigscore --fix
# Apply auto-fixes
npx github:Back-Road-Creative/rigscore --fix --yes
# Apply auto-fixes AND install the starter packs that target your red checks
npx github:Back-Road-Creative/rigscore --fix --yes --install-packs
# Use a scoring profile
npx github:Back-Road-Creative/rigscore --profile minimal
# Watch mode — re-run on config changes
npx github:Back-Road-Creative/rigscore --watch
# Install a pre-commit hook
npx github:Back-Road-Creative/rigscore --init-hookUsing the pre-commit framework? Add rigscore to your .pre-commit-config.yaml (it's backed by .pre-commit-hooks.yaml at this repo root):
repos:
- repo: https://github.com/Back-Road-Creative/rigscore
rev: v2.1.0
hooks:
- id: rigscore
# args: [--fail-under=80, --ci] # optional: tune the gateThis is framework-managed — pre-commit clones and installs rigscore itself (language: node, no npm publish needed) — as opposed to rigscore --init-hook above, a native git-hook installer that writes a pinned npx line straight into your repo's .git/hooks/pre-commit.
| Platform | Status | Notes |
|---|---|---|
| macOS | Supported | CI runs macos-latest. Homebrew Node is fine. infrastructure-security returns N/A (Linux-only). |
| Linux | Supported | Native. All checks apply, including infrastructure-security. |
| WSL2 | Supported | Treat as Linux. See WSL2 gotcha below. |
| Docker Desktop | Supported | Pull the image, bind-mount the target dir. See CI Integration and the Dockerfile in the repo. |
| Git Bash / MSYS2 | Unsupported | POSIX-only permission checks fail silently. Use WSL. |
| Windows native | Out of scope | POSIX permission assumptions and shell-command calls mean behavior can't be made reliable. Use WSL2. |
WSL2 gotcha — NTFS-mounted paths. When you scan a project under /mnt/c/... (Windows-owned NTFS), POSIX permission bits don't map cleanly. permissions-hygiene may emit spurious world-readable / mixed-UID findings on files owned by the Windows side. Workaround: keep projects inside the WSL filesystem (e.g., ~/projects/...) and scan there. If you must scan a /mnt/ path, suppress the affected finding via --ignore permissions-hygiene/sensitive-file-world-readable or via .rigscorerc.json suppress:.
-
GitHub-only. rigscore is distributed via
npx github:Back-Road-Creative/rigscore. It is not published to npm. SeeCLAUDE.mdfor the full rationale — in short: npm publish was intentionally dropped in v0.8.0 (commit #62) to keep the supply-chain surface tight. The tool is the sort of thing you want to audit before running; pulling straight from GitHub makes the audit trail obvious and avoids a second supply-chain hop. -
Docker image (GHCR). The Docker Publish workflow (
.github/workflows/docker-publish.yml) builds and publishesghcr.io/back-road-creative/rigscore:<tag>automatically onv*.*.*tag pushes. It is also callable viaworkflow_dispatchfor manual and dry-run builds. Pull a published tag asghcr.io/back-road-creative/rigscore:<tag>. -
GitHub Action.
action.ymlat the repo root exposes rigscore as a composite GitHub Action. Reference it from a workflow asuses: Back-Road-Creative/rigscore@v2.1.0. The action requires an exactvX.Y.Ztag — floating refs (@v1,@v2,@main) are rejected at run time to prevent supply-chain drift, and no moving major tag is published. Copy-paste job:name: rigscore on: [push, pull_request] jobs: rigscore: runs-on: ubuntu-latest permissions: contents: read security-events: write # required for upload-sarif steps: - uses: actions/checkout@v4 - uses: Back-Road-Creative/rigscore@v2.1.0 # MUST be an exact vX.Y.Z tag — floating refs are rejected with: fail-under: '70' upload-sarif: true
-
Cross-platform support. CI runs against
ubuntu-latestandmacos-latestacross Node18.17,20, and22(.github/workflows/ci.yml). WSL users get the Linux path. Windows native is out of scope — POSIX-only permission checks and shell-command assumptions make it a separate workstream, not a v1.0.0 deliverable.
MCP (Model Context Protocol) lets AI agents connect to external tools via servers. Each server exposes capabilities — filesystem access, API calls, database queries. The security risk is in the permissions.
rigscore scans MCP configs across all major clients: Claude (.mcp.json, .vscode/mcp.json), Cursor (~/.cursor/mcp.json), Cline (~/.cline/mcp_settings.json), Continue (~/.continue/config.json), Windsurf (~/.windsurf/mcp.json), Zed (~/.config/zed/settings.json), Amp (~/.amp/mcp.json), Gemini CLI (.gemini/settings.json), opencode (opencode.json, servers under the mcp key), Claude Desktop (~/.claude/claude_desktop_config.json), Amazon Q Developer (.amazonq/mcp.json, .amazonq/default.json, ~/.aws/amazonq/), Roo Code (.roo/mcp.json), Cody (cody.mcpServers in .vscode/settings.json), JetBrains Junie (.junie/mcp/mcp.json, ~/.junie/mcp/mcp.json), Warp (.warp/.mcp.json, ~/.warp/.mcp.json), Kiro (.kiro/settings/mcp.json, ~/.kiro/settings/mcp.json), Qwen Code (.qwen/settings.json, ~/.qwen/settings.json), Crush (.crush.json, crush.json, ~/.config/crush/crush.json — servers under the mcp key), OpenClaw (~/.openclaw/openclaw.json — servers under the nested mcp.servers key), and Antigravity (~/.gemini/antigravity/mcp_config.json).
What rigscore looks for:
- Transport type:
stdio(local, safer) vs.sse(network, riskier) - Wildcard environment passthrough (
env: {...process.env}) — exposes all your env vars to the server - Filesystem scope: is the server limited to project directories, or does it have access to
/? - Version pinning: are packages locked to specific versions, or using
@latest? - Cross-client configuration drift: are the same servers configured differently across clients?
- Typosquatting detection: is a package name suspiciously close to a known MCP server?
Supply chain risk: An MCP server installed as @latest today could push a malicious update tomorrow. Version pinning prevents this. {#mcp-supply-chain}
MCP registry augmentation (--online): when --online is passed, rigscore augments the hand-curated typosquat list with data from the official MCP registry at https://registry.modelcontextprotocol.io/v0/servers. The response is cached at ~/.cache/rigscore/mcp-registry.json (XDG convention; overridable via XDG_CACHE_HOME only when it resolves inside $HOME — a value pointing outside your home directory is silently ignored and the cache falls back to ~/.cache) with a 24-hour TTL. If the refetch fails after expiry, the stale cache is still used and an INFO finding surfaces the state. The registry only augments the hand-curated list — it never replaces it. Pass --refresh-mcp-registry to force a refetch (implies --online).
Air-gapped / offline pre-populate: place a JSON file at ~/.cache/rigscore/mcp-registry.json with the shape {"fetchedAt": "<ISO-8601 timestamp>", "data": { ... raw /v0/servers response ... }}. rigscore will treat it like any other cache entry and respect the 24h TTL. An air-gapped host can refresh the file from a machine with network access on whatever cadence it chooses.
What to fix: Scope filesystem servers to your project directory only. Remove wildcard env passthrough — pass only the specific variables each server needs. Pin all server packages to exact versions. Prefer stdio transport unless you specifically need network access.
The coherence check is rigscore's second pass — it compares what your governance file claims against what your actual configuration does. This catches contradictions that no single check can see.
What rigscore looks for:
- Governance claims "no external network" but MCP uses network transport
- Governance claims "path restrictions" but MCP has broad filesystem access
- Governance claims "forbidden actions" but Docker is running privileged
- MCP configuration drifts across AI clients without governance guidance
- Governance claims anti-injection rules but skill files contain injection patterns
- Compound risk: data exfiltration patterns combined with broad filesystem access
settings.jsondisables approval gates that CLAUDE.md requires
Compound risk penalty: If the coherence check finds a CRITICAL-severity contradiction, 10 points are deducted from the overall score on top of the per-check penalty. This reflects the systemic nature of governance failures.
Skill files (.cursorrules, .windsurfrules, .continuerules, .roorules, .goosehints, .junie/guidelines.md, copilot-instructions.md, AGENTS.md, .aider.conf.yml) tell AI agents how to behave. They're also a prompt injection vector — malicious instructions embedded in skill files can override agent behavior.
What rigscore looks for:
- Instruction override patterns ("ignore previous instructions", "disregard", "new system prompt")
- Shell execution instructions embedded in skill files
- External URL references (potential data exfiltration)
- Base64 or encoded content (obfuscated payloads)
- File permissions (writable by others?)
What to fix: Audit all skill files for unexpected instructions. Lock file permissions so only you can modify them. Be cautious with skill files from untrusted sources — treat them like executable code, because that's effectively what they are.
Scope: By default rigscore only scans project-level skill/command dirs (cwd) — .claude/commands, .claude/skills, and the per-client command dirs (.opencode/commands, .gemini/commands), all derived from the client registry. Home-level skills are user-global and not attributable to a project, so they do not deduct from the project score. Pass --include-home-skills to also scan every registered client's home dir (~/.claude/skills, ~/.claude/commands, ~/.codex/prompts, ~/.config/opencode/commands, ~/.gemini/commands); findings from home files are labeled with a ~/ prefix.
Your CLAUDE.md file tells AI agents what they can and can't do. Without one, your agent operates with no explicit rules — it can access any file, run any command, and make any API call that its underlying permissions allow.
rigscore recognizes governance files for all major AI coding clients: CLAUDE.md, .cursorrules, .windsurfrules, .clinerules, .continuerules, .roorules, .goosehints, .junie/guidelines.md, QWEN.md, CRUSH.md, copilot-instructions.md, AGENTS.md, and .aider.conf.yml. It also scans the modern directory-form rule sets by default — .cursor/rules/*.mdc, .windsurf/rules/, .clinerules/ (directory), .github/instructions/*.instructions.md, .amazonq/rules/*.md (Amazon Q), and .kiro/steering/*.md (Kiro) — so a repo governed only by those is not mis-scored as ungoverned.
What rigscore looks for:
- Does a governance file exist in the project root?
- Does it contain forbidden action rules with proper negation context?
- Does it have human-in-the-loop approval gates?
- Does it restrict file and directory access?
- Does it restrict network and API access?
- Does it include anti-injection instructions?
- Is the governance file tracked in git (not ephemeral)?
- Does it define TDD/pipeline lock, Definition of Done, and git workflow rules?
A good CLAUDE.md is not a wishlist — it should define specific, enforceable boundaries. rigscore checks that your governance file documents key security dimensions; enforcement depends on your tooling (hooks, permissions, container isolation). {#claude-md-hardening}
Starter templates for each AI client (Cursor, Cline, Continue, Windsurf, Aider) live in docs/examples/ — copy the one for your client and customize.
What to fix: Create a governance file with explicit execution boundaries, forbidden actions, file access restrictions, and approval gates. Be specific — "don't access sensitive files" is too vague. List the exact directories and operations that are off-limits.
.claude/settings.json controls what Claude Code can do autonomously. Certain settings — individually or in combination — can eliminate human oversight entirely.
What rigscore looks for:
enableAllProjectMcpServers— auto-trusts all MCP servers without per-project approvalskip-permissions— disables the permission gate for file and command operations- Hook configurations that shell out to arbitrary commands
- Bypass combos: pairings of settings that together eliminate all security gates
When enabled with --deep, rigscore recursively scans your source files for hardcoded secrets. This goes beyond the root config file scanning and checks .js, .ts, .py, .go, .rb, .java, .yaml, .json, .toml, .sh, and .env.* files.
What rigscore looks for:
- ~45 secret patterns: API keys from Anthropic, OpenAI (current
sk-proj-/sk-svcacct-and legacysk-keys), AWS (including STS tokens), GitHub (classic + OAuth + user/server/refresh tokens + fine-grainedgithub_pat_), GitLab, Slack, Stripe, SendGrid, Twilio, Firebase/Google API keys, Google OAuth client secrets (GOCSPX-), DigitalOcean, Mailgun, npm, PyPI, Hugging Face, Shopify, Databricks, MongoDB, Vercel, Supabase, Cloudflare, Railway, PlanetScale, Neon, Linear, Replicate, Tavily, webhook signing secrets, AGE encryption keys, Datadog, 1Password CLI references, HashiCorp Vault, JFrog Artifactory, and Docker registry auth tokens. Patterns are anchored and length-bounded to keep false positives low; for git-history and entropy-based deep scans, pair rigscore with a git-history / entropy secret scanner. - Comment vs. hardcoded distinction (commented/example keys are
info, real keys arecritical) - Skips test files, node_modules, .git, vendor, dist, build directories
What to fix: Move secrets to .env files or a secrets manager. Use environment variables in your application code.
API keys, tokens, and credentials in the wrong places are the most common security failure in any codebase — and AI development makes it worse because agents read config files, skill files, and environment variables as part of their normal operation.
What rigscore looks for:
.envfiles present but not in.gitignore- API key patterns in config files, governance files, and the
envmaps of committed MCP configs (skill files are covered by the skill-file safety check, not here) .envfile permissions (world-readable vs. user-only)- SOPS encryption detection
What to fix: Add .env to .gitignore immediately. Set .env permissions to 600 (user read/write only). Never hardcode API keys in governance or config files. Use environment variables and pass them explicitly.
Checks where credentials actually live — env vars in the right files, committed secrets in the wrong ones. Broader pattern coverage than the secret exposure check.
Containers provide isolation for AI agent workloads — but misconfigured containers can actually increase your attack surface instead of reducing it.
rigscore scans Docker Compose, Podman Compose, Kubernetes manifests, and devcontainer.json configurations. Compose include directives are followed and analyzed. K8s manifests are scanned in the project root and common subdirectories (k8s/, kubernetes/, manifests/, deploy/), including multi-document YAML files.
What rigscore looks for:
- Docker socket (
/var/run/docker.sock) mounted in containers — this is a container escape vector {#docker-socket-risk} privileged: true— gives the container full host access- Volume/hostPath mounts to sensitive host directories — the exact-match set is
/,/etc,/root,/home - Host network mode — bypasses container network isolation
- Missing
userdirective (container runs as root) - Missing
cap_drop: [ALL](retains default Linux capabilities) - Missing
no-new-privilegessecurity option - Missing memory limits
- K8s-specific:
hostPID,hostIPC,allowPrivilegeEscalation, missingrunAsNonRoot - Devcontainer:
--privilegedin runArgs, dangerous capability additions
What to fix: Never mount the Docker socket unless absolutely necessary. Never run containers in privileged mode. Scope volume mounts to project directories only. Add user, cap_drop: [ALL], and no-new-privileges to every service. Set memory limits.
Host-level controls that sit beneath your project: root-owned git hooks, a git wrapper that cannot be bypassed, a shell safety guard, and immutable governance directories. These are the backstop when per-project hooks or settings are missing or tampered with. This check is Linux-only — it returns N/A on macOS and Windows.
What rigscore looks for:
- A managed git hooks directory (default: the repository's own, resolved via
git rev-parse --git-path hooks; point it at a root-owned system dir such as/opt/git-hooks/via.rigscorerc.json→paths.hooksDir) - That directory is owned by root
- Required hooks present and executable:
pre-commit,pre-push,commit-msg - A git safety wrapper at
/usr/local/bin/git(configurable) that is root-owned and strips--no-verifyto prevent hook bypass - A shell safety guard at
/etc/profile.d/safety-gates.sh(configurable) that blocks dangerous patterns likechmod 777 - Immutable flag (
chattr +i) on any directories listed in.rigscorerc.json→paths.immutableDirs(empty by default — nothing is inspected here unless you configure it) permissions.denylist in~/.claude/settings.jsonincludes dangerous patterns:git push --force,git reset --hard,rm -rf,git push origin main,git push origin master- A
sandbox-gatehook registered underPreToolUseto gate Write/Edit/Bash
Checks skill files and CLAUDE.md for hidden characters that render identically to legitimate text but redirect agent behavior. Covers the attack surface from the ToxicSkills and Rules File Backdoor incidents.
What rigscore looks for:
- Greek/Cyrillic/Armenian/Georgian/Cherokee lookalikes that render as Latin letters
- Zero-width joiners and zero-width non-joiners
- Bidirectional control characters (bidi overrides)
- Tag characters (U+E0000–U+E007F) — an invisible ASCII-shadow channel
Git hooks are your last line of defense before code leaves your machine. Without pre-commit hooks, secrets, broken governance files, and unreviewed changes go straight to the repo.
What rigscore looks for:
- Pre-commit hooks present (
.git/hooks/pre-commitor a hook manager like Husky/lefthook) - Claude Code hooks (
.claude/settings.jsonwith hook configuration) - Push URL guards (
.git/configwithpushurl = no_push) - External hook directories from config
What to fix: Install Husky or lefthook and add pre-commit hooks that scan for secret patterns and validate governance files.
File permissions are the foundation of access control. Misconfigured permissions on SSH keys, secret files, or governance files can undermine every other security measure.
What rigscore looks for:
- SSH directory permissions (
~/.sshshould be 700) - SSH private key permissions (should be 600)
- World-readable sensitive files in the project (
.pem,.key,*credentials*) - Governance file ownership consistency (mixed UIDs may indicate unauthorized modifications)
What to fix: Run chmod 700 ~/.ssh and chmod 600 ~/.ssh/id_*. Ensure sensitive files are not world-readable. Verify all governance files are owned by the same user.
Platform note: Permission checks are POSIX-only. On Windows, rigscore reports a SKIPPED finding and recommends manual verification with icacls.
On Windows, rigscore checks for WSL-specific security risks. This is an advisory check — it doesn't affect the score but surfaces important configuration issues.
What rigscore looks for:
- WSL interop settings — warns if Windows PATH leaks into WSL (
appendWindowsPath=true) .wslconfigfirewall and networking mode- Windows Defender exclusions that include project directories or
node_modules - NTFS permissions advisory for sensitive files
Platform note: Runs on Windows hosts and on WSL2 Linux guests (the WSL arm is marker-based — it fires when the kernel release string contains microsoft, so a WSL guest is graded even though it is Linux). Returns N/A only on plain Linux and macOS. Weight 0 means it never affects the score.
Advisory check that detects AI services bound to 0.0.0.0 instead of 127.0.0.1. Scans MCP config URLs, Docker port bindings, Ollama config, and live listeners.
Probes deployed sites listed in .rigscorerc.json under sites: [...]. Only runs when --online is passed; otherwise returns N/A.
What rigscore looks for:
- Critical security headers:
content-security-policy,x-frame-options,x-content-type-options,strict-transport-security - Advisory headers:
referrer-policy,permissions-policy - Server fingerprinting via
X-Powered-Byand versionedServerheaders - Sensitive paths that should not be publicly reachable (
.env,.git/config,backup.zip,wp-admin/,phpmyadmin/, etc.) - PII leakage in rendered HTML (emails, phone numbers, internal IP ranges)
- Hardcoded API keys in the served page source
<meta name="generator">build-tool disclosure- SSL certificate expiry (critical if expired, warning if <30 days)
Audits the instruction files that feed the agent — CLAUDE.md, ~/.claude/CLAUDE.md, and files under .claude/commands/ and .claude/skills/. Measures quality, not security, so it is advisory.
What rigscore looks for:
- Context budget: total estimated tokens across all instruction files versus a 200K reference window (warns above 20%, info above 10%); flags individual files above ~5K tokens
- Bloat: files over 500 lines (warning) or 300 lines (info)
- Vague directives: phrases like "use your best judgment", "as appropriate", "figure it out", "when it makes sense"
- Contradictions between
always/mustandnever/must notdirectives on the same topic - Dead file references in backtick paths and markdown links (paths that no longer exist)
- Redundancy across instruction files
Scans every SKILL.md under .claude/skills/ and .claude/commands/ (both project and ~/.claude/) and checks that each skill is aware of the constraints that governance claims to enforce. Advisory.
What rigscore looks for:
- Skills that perform git/ship/push operations but don't acknowledge manual merge workflow requirements (
gh-merge-approved,brc-merge-approved) - Skills that perform write/edit/scaffold operations but don't acknowledge layer write restrictions on
_governance/and_foundation/ - Skills that overwrite files without mentioning WIP protection (untracked files in
_active/svc-*have no backup) - Skills that push/commit/ship without mentioning branch protection (no force push, no direct push to main/master)
- Hook ↔ settings conflicts where a PreToolUse hook blocks a pattern that
settings.jsonallow-lists
Opt-in by default. The detections above are driven by .rigscorerc.json → skillCoherence.constraints and skillCoherence.hookSettingsConflicts, both of which ship empty — a stock install emits nothing from this check until you configure the constraints your workspace enforces. The gh-merge-approved / _governance/ / _active/svc-* examples are the conventions this repo configures, not built-in defaults.
Classifies the project's workflow artefacts against the AI development taxonomy and surfaces graduation signals — skills that should become code, pipelines that should be split, orphan memory files. Advisory.
What rigscore looks for:
- Pipeline step overload: single modules with too many
# Stage N/# Step N/# Phase Nmarkers, or stage directories that suggest a monolithic pipeline should be decomposed - Skills that have matured past the LLM-driven stage and should be graduated to deterministic code
- Orphan memory files that are not linked from
MEMORY.md(workflow-maturity/memory-orphan)
Scans .claude/agents/*.md (project and ~/.claude/) and verifies that every agent that claims to emit JSON output (e.g. body contains "Return ONLY a JSON" or an "## Output Format" section) declares a parseable ```json fenced example block. Codifies the convention documented in _active/lib-skill-utils/AGENT_OUTPUT_SCHEMAS.md so fan-out orchestrators have a reliable contract to parse against.
What rigscore looks for:
- Agents that announce JSON output but contain no
```jsonfenced example block - Agents whose
```jsonfenced blocks failJSON.parse(placeholder values left unquoted, trailing commas, comments)
Enforces the docs-first gate: every module in src/checks/ must have a matching page under docs/checks/ with the canonical sections (Purpose, Triggers, Weight rationale, Fix semantics, SARIF, Example) filled in, and every doc must correspond to a real check. Advisory, and only active in rigscore itself or plugin repos that mirror its layout. See docs/checks/documentation.md.
What rigscore looks for:
- Check modules in
src/checks/with no matchingdocs/checks/<id>.md - Doc pages that are missing required H2 sections or have empty section bodies
- Doc pages whose stated weight drifts from
src/constants.js - Doc H1 titles that do not match the check id
- Orphan doc pages with no corresponding check module
Scores how safely a repo runs agent loops — anywhere the project drives an agent CLI repeatedly with no human in the loop. An agent loop must be bounded (iteration cap, turn budget, or timeout) and must be able to stop. Most repos have no agent-loop surface and return N/A. First check of the Practice pillar. See docs/checks/loop-governance.md.
What rigscore looks for:
- Agent invoked inside a loop with no bound — no iteration counter, no
--max-turns, notimeout - Loops with no terminal state to test for — stoppable only by killing them
- Agents on a cron line with nothing bounding one unattended tick
--dangerously-skip-permissionsin any scanned script
Scope note: agent jobs in .github/workflows/** belong to ci-agent-caps and are deliberately ignored here, so one uncapped CI job is not double-counted.
Scores whether a repo drives its agents from written goals and specs. Maps to ASI01 — an agent with no stated goal has nothing to be hijacked away from. Recognises four layouts (GitHub Spec Kit, AGENTS.md, Kiro, OpenSpec), each gated on its marker dir rather than a bare specs/ dir. See docs/checks/spec-goals.md.
What rigscore looks for:
- A governing goal file that is still an unfilled boilerplate template
- Specs that were never decomposed into executable tasks
- Goal-file staleness: a 90-day gap against the newest spec, by local git committer date (offline, read-only)
An agent running unattended in CI — repo write credentials, no human at the keyboard — is the highest-blast-radius agent surface most teams have. Every CI job invoking an agent should declare a turn cap, a job timeout, and tool scoping. See docs/checks/ci-agent-caps.md.
What rigscore looks for:
- Bypass flags in a workflow (
--dangerously-skip-permissions,--yolo,--sandbox danger-full-access, etc.) — CRITICAL - Agent jobs with no
timeout-minutes(silently inheriting GitHub's 360-minute default) - Agent invocations with no turn cap (
--max-turns) or with unrestricted tools
Agent memory is re-injected every turn, so bloat is billed per request and junk competes with governance for the model's attention. No incumbent convention exists for memory layout — this check defines one. Budget default is configurable via .rigscorerc.json (memoryHygiene.budgetBytes). See docs/checks/memory-hygiene.md.
What rigscore looks for:
- The auto-loaded memory bundle exceeding a byte budget (default 40,000)
- Memory files that are empty, or a bare stub with frontmatter and no body
- Single home per rule: the same rule line written verbatim into both a governance file and a memory file, so editing one copy silently fails to take effect
Projects that accept AI-assisted contributions are increasingly expected to say so. Applicable only when the repo shows an AI surface (a governance file, a .claude/ dir, an MCP config, or an agent CI job); a repo with no AI surface owes no disclosure and returns N/A. Advisory because the conventions are still forming. See docs/checks/ai-disclosure.md.
What rigscore looks for:
- An AI surface with no AI-use policy anywhere (
AI_POLICY.md, or a generative-AI section inCONTRIBUTING.md) - A PR template that carries no generative-AI declaration
- No PR template in any location GitHub reads (INFO — the weakest of the three signals)
Every agent CLI expresses "how much can this agent do without asking me" differently. This check reduces an agent's sandbox config to a single posture — restricted / partial / unrestricted — and flags the dangerous combinations, principally an agent that is both free of approval prompts and able to reach the network. The client registry (src/clients.js) is the surface list, so a new client is picked up with no change to this check's logic. See docs/checks/sandbox-posture.md.
What rigscore looks for:
- Codex CLI (
.codex/config.toml):approval_policy,sandbox_mode,[sandbox_workspace_write] network_access - Claude Code (
.claude/settings.json+settings.local.json): whether anypermissions.denyrules exist at all, plusdefaultMode. Which allow entries are dangerous isclaude-settings' job; this check never re-grades them. - Gemini CLI (
.gemini/settings.json):general.defaultApprovalMode(default/auto_edit/plan/yolo) - opencode (
opencode.json): thepermissionblock — whetherbash(or the*catch-all) auto-runs (allow) vs prompts (ask) / blocks (deny) - Cursor (committed
.cursor/permissions.json): a"*"/"*:*"wildcard interminalAllowlist/mcpAllowlistthat auto-runs everything
Windsurf is deliberately not graded: its Turbo-mode auto-execute level and allow/deny lists live only in the Windsurf Settings UI / global config, with no committed in-repo file rigscore can read at cwd.
Opt-in check for obfuscated MCP tool poisoning — a hidden directive paraphrased into a tool description so static regex checks miss it. Runs only with --semantic; a default scan makes zero external calls from it. For each tool description (read from tools/list snapshot files listed under paths.mcpToolsSnapshot in .rigscorerc.json — the same JSON you pipe into rigscore mcp-hash), it asks your own first-party agent CLI (claude -p, gemini, codex exec, … — never an API key or SDK client) to classify the text benign vs. suspicious. The command is configurable via semantic.command in .rigscorerc.json (default ["claude", "-p"]; the judge prompt is appended as the final argument). Each description is wrapped in a data-only frame and the judge is told to treat it as data, not instructions, so a poisoned description cannot hijack the judge. If that binary is not on PATH the check skips gracefully (no finding, no crash). See docs/checks/semantic-tools.md.
| Score | Grade | Meaning |
|---|---|---|
| 90-100 | A | Strong hygiene posture |
| 75-89 | B | Good foundation, some gaps |
| 60-74 | C | Moderate risk, needs attention |
| 40-59 | D | Significant gaps |
| 0-39 | F | Critical issues, fix immediately |
Scoring uses an additive deduction model with moat-heavy weighting — AI-specific checks (MCP, coherence, skill files, governance) account for ~48% of the score:
| Check | Weight | Category |
|---|---|---|
| MCP server configuration | 14 | supply-chain |
| Cross-config coherence | 14 | governance |
| Skill file safety | 10 | supply-chain |
| CLAUDE.md governance | 10 | governance |
| Claude settings safety | 8 | governance |
| Deep source secrets | 8 | secrets |
| Secret exposure | 8 | secrets |
| Credential storage hygiene | 6 | secrets |
| Docker security | 6 | isolation |
| Infrastructure security | 6 | process |
| Unicode steganography | 4 | governance |
| Permissions hygiene | 4 | process |
| Git hooks | 2 | process |
| Windows/WSL security | 0 | isolation (advisory) |
| Network exposure | 0 | isolation (advisory) |
| Site security | 0 | isolation (advisory) |
| Instruction effectiveness | 0 | governance (advisory) |
| Skill ↔ governance coherence | 0 | governance (advisory) |
| Workflow maturity | 0 | governance (advisory) |
| Documentation coverage | 0 | process (advisory) |
| Agent output schemas | 0 | governance (advisory) |
| Loop governance | 0 | process (advisory) |
| Spec goals | 0 | governance (advisory) |
| CI agent caps | 0 | process (advisory) |
| Agent memory hygiene | 0 | governance (advisory) |
| AI-use disclosure | 0 | governance (advisory) |
| Sandbox posture | 0 | isolation (advisory) |
| Semantic tool-description judge | 0 | supply-chain (advisory) |
- CRITICAL findings zero out their sub-check entirely
- WARNING findings deduct 15 points each (1 WARNING = 85, 2 = 70, 3 = 55...)
- INFO findings deduct 2 points each, with a floor of 50 when no WARNINGs are present
- PASS and SKIPPED findings have no score impact
Compound risk penalty: When the coherence check finds a CRITICAL contradiction, 10 additional points are deducted from the overall score — reflecting the systemic nature of governance failures.
Coverage scaling (what the Coverage: N of M checks applicable (weight W/100) line means): Checks that find nothing to scan are marked N/A and excluded from the weighted average — their weight is redistributed proportionally across the remaining applicable checks. The final overall score is then multiplied by min(1, W / 100), where W is the total applicable check weight. This scaling is continuous and always applied — there is no threshold and no step. A project where only part of the check suite can reach a verdict should not be able to claim a perfect 100 — partial coverage means partial confidence.
Put plainly: W is your reachable ceiling. If only 80 of the 100 points of check weight apply, then even an all-passing scan caps at 80/100 — and the report says so on the coverage line (— score scaled ×0.80). Full coverage (W ≥ 100) is a no-op. The compound risk penalty above is applied after scaling, as a flat 10-point deduction (it is not itself scaled). Characterization tests pin the exact behavior in test/scoring-coverage.test.js; see the design note at the top of src/scoring.js for the rationale.
Every run computes a second 0–100 axis — the Practice score — printed under the hygiene score in the box above and exposed in --json as practiceScore. Where the hygiene (Security) score measures configuration safety, Practice measures operator practice: it is built from the Practice-pillar advisory checks (loop-governance, spec-goals, ci-agent-caps, memory-hygiene). Those checks are weight-0 on the Security axis, so the Practice score is fully independent — it never moves your hygiene score.
A repo with no practice surface to grade — no agent loops, specs, CI agent jobs, or memory files — reports Practice: n/a, never 0/100, so a project that is simply out of scope for these checks is not penalised. See docs/practice-score.md for the per-check math.
Scoring profiles: Five built-in profiles:
default— balanced AI dev environment audit (WEIGHTS fromsrc/constants.js).minimal— AI-moat checks only (mcp-config 30, coherence 30, skill-files 20, governance-docs 20; rest 0).ci— CI pipelines (identical todefaulttoday).home— single-user dev boxes /~/scans. Governance / skill-files / MCP emphasized; infra / docker / windows off so coverage-scaling doesn't punish N/A infra surfaces.monorepo— same weights asdefaultwith hints for--recursive --depth 3.
See docs/profiles/ for full weight tables. Custom weights can be set in .rigscorerc.json. Advisory (weight-0) checks NEVER affect the overall score — they are excluded from the coverage-penalty denominator so adding a new advisory cannot shift existing scores.
Advisory checks ship at weight 0 — they never affect the score. If your
team cares about a specific advisory signal (e.g. prompt bloat from
instruction-effectiveness, workflow maturity, skill↔governance
coherence), promote it via .rigscorerc.json:
{
"weights": {
"instruction-effectiveness": 5
}
}Promoting an advisory to a non-zero weight moves it out of the advisory lane and into the scored lane in full. Specifically:
- It now contributes to the overall score with whatever weight you set.
- It now counts toward coverage — the denominator that drives
coverage-scaling for low-applicability projects. Before promotion, a
weight-0 check is explicitly excluded from coverage math (see
src/scoring.js—scoringApplicablefilter). After promotion, an N/A verdict on that check (e.g. a project with no skills dir forskill-coherence) will redistribute weight like any other N/A check. - If you promote an advisory across a whole org, document it in your
.rigscorerc.jsoncomment so the delta from the public default score is traceable.
Demoting a scored check (setting weights.<id>: 0) is the inverse and has
the same coverage semantics.
rigscore is offline and stateless by default. A few paths may be written on first scan — all are local, none phone home.
| Path | When written | Purpose |
|---|---|---|
<cwd>/.rigscore-state.json |
Only when the project has a repo-level MCP config — any of the 15 committed client paths (.mcp.json, .cursor/mcp.json, .vscode/mcp.json, .gemini/settings.json, opencode.json, and the Amazon Q / Roo / Junie / Warp / Kiro / Qwen / Crush variants), not just .mcp.json. |
Stores SHA-256 hashes of each MCP server's {command, args, envKeys} shape. Detects silent MCPoison-class (CVE-2025-54136) pivots on subsequent scans. Values are never hashed — only env-var keys. |
$XDG_CACHE_HOME/rigscore/mcp-registry.json (falls back to ~/.cache/rigscore/mcp-registry.json) |
Only with --online or --refresh-mcp-registry. |
Caches the official MCP registry response. 24h TTL. Used to augment typosquat detection. |
<cwd>/.rigscore-history.json |
Only with --record-score — a plain scan never writes it. |
Appends one row per recorded scan (score, grade, timestamp) so --trend can print the history and per-run deltas. Local scores only, no findings and no file contents. |
Recommendations:
- Commit
.rigscore-state.json— do not.gitignoreit. It is the detection substrate: a pin that isn't in git cannot survive a fresh CI checkout, so gitignoring it silently turns rug-pull detection off where it matters most. It stores hashes only — never env values — so it is safe to commit. See State file. - To skip the write entirely, pass
--no-state-write— and read what it costs you in State file first. - Nothing else is persisted. No telemetry, no accounts, no outbound
traffic unless you pass
--online.
To purge all rigscore state:
rm -f .rigscore-state.json
rm -rf "${XDG_CACHE_HOME:-$HOME/.cache}/rigscore"rigscore is a configuration presence checker, not a security enforcement tool. Understanding its scope helps you use it effectively. Read this section before you rely on rigscore as a governance quality signal.
- Semantic reversal bypasses keyword checks (known limitation — #1 thing to understand). rigscore's governance checks (CLAUDE.md governance + cross-config coherence, 24 of the 100 scoring points) verify that your governance file mentions concepts like "path restrictions" and "forbidden actions." A CLAUDE.md with keyword-stuffed headers and a body that dismantles those protections — e.g.,
# Path Restrictions\nAll paths are available for maximum productivity.— passes the keyword check. rigscore does not read for semantic intent. Seetest/keyword-gaming.test.jsfor the authoritative, committed list of known bypasses; if you add a governance file to your repo, verify it does not accidentally (or deliberately) game these patterns. The only mitigation that ships today is the cross-config coherence pass, which cross-checks governance claims against observed configuration. LLM-judge assist (opt-in) is a planned roadmap item — it is not implemented (see Roadmap). - Injection detection is pattern-based. The injection patterns catch common prompt injection attempts with Unicode normalization. Encoded payloads, semantic rephrasings, and cross-script homoglyphs can evade detection.
- The default scan pins config-shape; runtime tool descriptions are pinned on demand. A scan hashes the configured shape of each MCP server —
{command, args, envKeys}— and warns when it changes between scans (CVE-2025-54136 / MCPoison class). Hashing the tool descriptions a running server advertises is handled by the separate, opt-inmcp-hash/mcp-verifyprint-and-paste workflow (Runtime tool pinning) — rigscore never spawns the server itself, so runtime drift is verified on demand rather than on every scan. See "State file" below. - Secret scanning covers named config files in the project root. rigscore checks the fixed set of names in
AI_CONFIG_FILES(src/constants.js) — every known governance file plusconfig.json,secrets.yaml,.env, and similar. For deep recursive scanning, use--deep. For git history scanning, use a dedicated git-history secret scanner. - Point-in-time snapshots only. No continuous monitoring or git history scanning. Use
--jsonor--sariffor CI pipeline integration. - Score is shape-dependent. Overall score reflects only the checks applicable to the project shape. rigscore ships 28 checks; an npm package sees most of them as N/A (no
.mcp.json, no Dockerfile, no.claude/skills/, no~/.sshto scan from CI, etc.) and scores accordingly. rigscore scores itself 37/100 in CI for this reason — only 10 of its own 28 checks are applicable — not because the project is broken. See Dogfooding below.
rigscore runs on rigscore in CI. Transparency about what that score means:
-
Self-score: 37/100 (Grade F). This is the real score, not a vanity baseline. rigscore is an npm package, so only 10 of its 28 checks are applicable (weight 46/100) — the rest legitimately return N/A (no MCP config, no Docker, no skill files, no
.claude/settings.json, etc.). The score is scaled by applicable coverage — the applicable weight over 100, capped at 1 — so partial coverage always means partial confidence (src/scoring.js), which is the intended behavior. Reproduce the CI number locally with a neutralised$HOME(what a runner sees):HOME=$(mktemp -d) node bin/rigscore.js --no-cta --profile default .
If your project is seeing similar — see
docs/TROUBLESHOOTING.mdfor the diagnostic walk-through. -
CI threshold:
--fail-under 30. Calibrated to the measured baseline (37) with a 7-point regression buffer. The public default is 70 — the gap is not a vanity choice, it reflects the project-shape reality above. A lower fail-under than the public default is normal for projects that don't exercise the full check surface; document yours the same way. -
.rigscorerc.jsondisables four checks (infrastructure-security,skill-coherence,workflow-maturity,agent-output-schemas) that require workspace-oriented artifacts rigscore doesn't ship. These are per-check disables at the profile level, not ignore-rules on individual findings — the distinction matters when auditing the config.
Planned work. Nothing in this section ships today — if it is listed here, it is not implemented.
- LLM-judge assist (opt-in). A semantic pass over governance files to catch the semantic-reversal weakness that keyword checks cannot see — a CLAUDE.md whose headers name the right concepts while its body dismantles them. Would be strictly opt-in and off by default, so rigscore stays fully offline and API-key-free out of the box.
npx github:Back-Road-Creative/rigscore # Scan current directory
npx github:Back-Road-Creative/rigscore /path/to/project # Scan a specific project
npx github:Back-Road-Creative/rigscore --json # JSON output for CI/scripting
npx github:Back-Road-Creative/rigscore --sarif # SARIF output for security tools
npx github:Back-Road-Creative/rigscore --cyclonedx # CycloneDX 1.6 AI-BOM: MCP servers + their grant surface (permission scopes), skills, rules (see docs/cyclonedx.md)
npx github:Back-Road-Creative/rigscore --report compliance # Group findings by compliance-framework control (see docs/compliance.md; not supported with --recursive)
npx github:Back-Road-Creative/rigscore --ci # CI mode (--sarif --no-color --no-cta)
npx github:Back-Road-Creative/rigscore --fail-under 80 # Fail if score < 80 (default: 70)
npx github:Back-Road-Creative/rigscore --profile minimal # AI-only scoring profile
npx github:Back-Road-Creative/rigscore --badge # Generate a score badge (format set by --badge-format)
npx github:Back-Road-Creative/rigscore --badge --badge-format svg # Badge format: markdown (default, shields.io image snippet), endpoint (shields.io Endpoint Badge JSON for a self-updating README), or svg (self-contained, no network). Also works with --recursive, badging the monorepo average
npx github:Back-Road-Creative/rigscore --junit # JUnit XML — one testcase per check (Jenkins, Azure Pipelines, GitLab test reports)
npx github:Back-Road-Creative/rigscore --code-quality # GitLab Code Quality report (CodeClimate JSON)
npx github:Back-Road-Creative/rigscore --no-color # Plain text output
npx github:Back-Road-Creative/rigscore --cta # Opt in to the promotional CTA (off by default)
npx github:Back-Road-Creative/rigscore --no-cta # Deprecated alias — CTA is already off by default; kept for back-compat
npx github:Back-Road-Creative/rigscore --check <id> # Run a single check by ID (if that check is N/A for the repo — e.g. --check docker-security with no Dockerfile — the score is `n/a` and the run exits 0, not a red 0/100)
npx github:Back-Road-Creative/rigscore --recursive # Scan subdirectories as projects
npx github:Back-Road-Creative/rigscore -r --depth 2 # Recursive scan, 2 levels deep
npx github:Back-Road-Creative/rigscore --deep # Deep source secret scanning
npx github:Back-Road-Creative/rigscore --online # Enable online checks (site-security, MCP supply chain)
npx github:Back-Road-Creative/rigscore --refresh-mcp-registry # Force refetch of the MCP registry cache (implies --online; bypasses 24h TTL)
npx github:Back-Road-Creative/rigscore --semantic # Opt-in semantic MCP tool-description judge (semantic-tools check; shells to a first-party agent CLI — `claude -p` default, configurable via semantic.command; no API key; skips if that binary absent)
npx github:Back-Road-Creative/rigscore --include-home-skills # Also scan every registered client's home dir (~/.claude, ~/.codex/prompts, ~/.config/opencode/commands, ~/.gemini/commands, …) (default: off — project scope only)
npx github:Back-Road-Creative/rigscore --fix # Show auto-fixable issues (dry run)
npx github:Back-Road-Creative/rigscore --fix --yes # Apply safe auto-remediations (edits existing files only — never scaffolds new ones)
npx github:Back-Road-Creative/rigscore --fix --yes --install-packs # Also install the starter packs that target your red checks (creates new files)
npx github:Back-Road-Creative/rigscore --watch # Watch for changes, re-run automatically
npx github:Back-Road-Creative/rigscore --init-hook # Install pre-commit hook
npx github:Back-Road-Creative/rigscore --ignore "env-exposure/env-not-gitignored,skill-files/shell-exec" # Suppress findings by finding ID (exact match, case-insensitive, comma-separated). See docs/FINDING_IDS.md for the stable ID list. Title-substring still works as a legacy fallback.
# Suppression is never silent: whether it comes from --ignore or a .rigscorerc.json `suppress:` entry, rigscore reports how many findings were muted and their ids — in the human report (so it shows up in a CI log), in JSON (`suppressed: { count, ids }`), and in SARIF (`runs[0].properties.suppressedCount` / `suppressedIds`). The findings are still removed from scoring; the muting is just visible, not only in a config diff. This holds in `--recursive` / `--profile monorepo` mode too: each project's own `.rigscorerc.json` `suppress:` is honored and rescored per project.
npx github:Back-Road-Creative/rigscore --verbose # Also show passing checks (info/skipped findings already print by default)
npx github:Back-Road-Creative/rigscore --quiet # Summary only — score, grade, posture and finding counts (pre-commit hooks, terse CI logs)
npx github:Back-Road-Creative/rigscore --record-score # Append this scan's score to .rigscore-history.json (opt-in; nothing is recorded without it)
npx github:Back-Road-Creative/rigscore --trend # Print the recorded score history and per-run deltas, then exit (read-only; runs no checks)
npx github:Back-Road-Creative/rigscore --version # Version info
npx github:Back-Road-Creative/rigscore --help # Show helpA .rigscorerc.json can inherit from one or more shared baselines with an
extends key, so a single hardened config is reusable across many repos:
// a project's .rigscorerc.json — inherits the baseline, adds its own bits
{
"extends": "../rigscore-base.json",
"sites": ["https://project-a.example"]
}Rules:
- Local paths only.
extendsis a string or an array of strings. A relative path resolves against the directory of the file that declared it; an absolute path works too. URLs (http://,https://) are rejected, never fetched — rigscore makes no external calls by default. (Node-module resolution likeextends: "some-pkg/base"is not supported yet; it may arrive in a future release.) - Precedence. An extended base is lower precedence than the file that
extends it — the extending file's own keys win. Within an
extendsarray, later entries override earlier ones (the ESLint convention). Arrays such asnetwork.safeHostsandsuppressstill concatenate-and-dedupe across every layer, per the merge policy above. - Recursive & cycle-safe. A base may itself
extendsanother base; the chain resolves depth-first. A cycle (a → b → a) is caught and raises a configuration error naming the loop rather than looping forever. - Errors. A missing target, a cycle, or a URL each fail the scan with a
configuration error (exit
2).
Both ~/.rigscorerc.json and a project .rigscorerc.json may declare their own
extends.
rigscore exits with a stable code so CI can branch cleanly:
| Code | Meaning |
|---|---|
0 |
Scan completed. Score is at or above --fail-under. In baseline/diff mode: no new findings vs baseline. |
1 |
Scan completed. Score is below --fail-under, OR (baseline mode) new findings were detected. |
2 |
Configuration error — malformed .rigscorerc.json, unknown --profile, invalid target directory, an unreadable or corrupt/malformed baseline file (unparseable JSON or no findings array fails closed — never silently re-minted), or bad input to mcp-hash / mcp-pin / mcp-verify. In a git repo the baseline gate reads the copy committed at HEAD (git show HEAD:<path>, like --verify-state), so a deleted/corrupt working-tree baseline can't launder findings. This also covers HEAD-deletion: a baseline that was tracked but removed at HEAD (a PR that git rm'd it) fails closed here rather than silently re-minting a fresh baseline over the new findings — distinguished from a genuine first run (never tracked, still mints + exit 0) by git history. Regenerate with --baseline-refresh then commit. |
3 |
Reserved for subcommand pre-conditions. Currently used by rigscore mcp-verify <server> when no runtime tool hash is pinned for that server. The main scan path does not emit 3. |
4 |
Runtime tool-description drift detected by rigscore mcp-verify <server> — the server's current tools/list hash differs from the pinned snapshot (CVE-2025-54136 "MCP rug pull" class). Re-pin with mcp-hash | mcp-pin if the change is intentional. The main scan path does not emit 4. |
A runtime crash inside a check surfaces as exit 2 with Error: scan failed: ... on stderr; it does not currently use a distinct code. CI authors should treat non-zero as failure and branch only on 0 vs 1 for score-gating logic.
Baseline mode gates on new findings rather than an absolute score — useful when you want CI to block regressions without first driving an existing repo to a clean score.
# First run writes the baseline; later runs report ONLY findings new vs it,
# and exit 1 if any appeared.
rigscore --baseline .rigscore-baseline.json .
# Intentionally accept the current findings as the new baseline, then commit it.
rigscore --baseline .rigscore-baseline.json --baseline-refresh .
# Standalone diff of two findings files (exit 1 if `current` adds any).
rigscore diff old-baseline.json new-findings.jsonIn a git repo the baseline is read from the copy committed at HEAD (like --verify-state), so a deleted or corrupt working-tree baseline can't launder findings. --baseline-refresh is the sanctioned way to (re)write the working-tree file for review and commit. Exit codes 0/1/2 follow the table above.
--watch re-runs rigscore automatically when relevant files change. It monitors governance files, MCP configs, .env, Docker Compose files, git hooks, and .rigscorerc.json. Changes are debounced (500ms) to avoid rapid re-scans.
npx github:Back-Road-Creative/rigscore --watch
npx github:Back-Road-Creative/rigscore --watch --verbose--init-hook installs a git pre-commit hook that runs rigscore before each commit:
npx github:Back-Road-Creative/rigscore --init-hookThis creates (or appends to) .git/hooks/pre-commit with npx github:Back-Road-Creative/rigscore --fail-under 70 --no-cta || exit 1. If the hook already contains rigscore, it skips installation.
For monorepos and multi-project workspaces, --recursive discovers project subdirectories and scans each independently. A directory is considered a project if it contains any recognizable marker file (package.json, pyproject.toml, Dockerfile, docker-compose.yml, CLAUDE.md, .env, etc.).
# Scan all projects one level deep
npx github:Back-Road-Creative/rigscore . --recursive
# Scan two levels (e.g., workspace/_active/svc-foo)
npx github:Back-Road-Creative/rigscore . -r --depth 2
# JSON output with per-project breakdown
npx github:Back-Road-Creative/rigscore . -r --depth 2 --json
# SARIF output with one run per project
npx github:Back-Road-Creative/rigscore . -r --depth 2 --sarifThe overall score uses the average across all discovered projects. Hidden directories, node_modules, venv, and __pycache__ are automatically skipped. Recursive scanning runs projects concurrently (4 at a time) for performance.
--fix identifies safe, reversible remediations and shows what would be changed:
# Dry run — see what would be fixed
npx github:Back-Road-Creative/rigscore --fix
# Apply fixes
npx github:Back-Road-Creative/rigscore --fix --yesSafe fixes only — most fixers self-register from their check modules (getRegisteredFixes()); a few mechanical ones that span several checks live in src/fixer.js itself. Today that is 17 fixers:
- Secrets / permissions: add
.envto.gitignore; add*.pem,*.keyto.gitignore;chmod 600on.envfiles;chmod 700on~/.ssh;chmod 600on SSH private keys;chmod 644on world-writable skill files. - MCP: disable the
enableAllProjectMcpServersauto-approve bypass in.claude/settings.json; strip anANTHROPIC_BASE_URL/ANTHROPIC_API_BASEredirect (CVE-2026-21852) from a committed MCP server env. - Docker: remove
privileged: true; remove a Docker-socket volume mount; addcap_drop: [ALL]; addsecurity_opt: [no-new-privileges:true]. - Coherence: append a governance-declaration stub for an undeclared MCP server (append-only — never rewrites existing prose).
- Unicode: strip zero-width, bidi-override and tag characters out of governance and committed-config files (invisible codepoints only — visible homoglyphs are left alone, since rewriting them is lossy).
- Git hooks:
chmod +xapre-commit/pre-pushhook git is silently skipping because it is not executable. - Permissions: fill a starter
permissions.denylist into a.claude/settings.jsonthat has none — only into a settings file you already have, and never over a deny list you already wrote. - Credentials: append a
${VAR}placeholder for a plaintext client-config credential to an.env.exampleyou already have, so the secret has somewhere to move to.
rigscore never modifies existing governance file content, and never creates a file the repo did
not already have — the last two fixers above edit an existing .claude/settings.json /
.env.example and are a no-op skip when that file is absent. Creating one is a pack install,
behind --install-packs.
--yes means "don't prompt me". It does not mean "scaffold governance files I never
asked for" — so --fix --yes only remediates checks that are already red, and creates no
file the repo did not already have.
A starter pack is the other kind of remediation: it drops a whole
baseline in (.claude/settings.json, a pre-commit hook, AGENTS.md). --fix still offers
the packs that target your red checks — listing them costs nothing — but installing them needs
its own consent:
# Offered, not installed: names the packs and the flag that would install them
npx github:Back-Road-Creative/rigscore --fix --yes
# Install them too
npx github:Back-Road-Creative/rigscore --fix --yes --install-packs--install-packs only widens what --yes may write — it never writes on its own, so
--fix --install-packs (no --yes) is still a dry run. An install never overwrites one of your
values: a file you are missing is written, and a json/yaml config you already have is hardened in
place by the additive merge engine — the pack's absent keys are merged in (reported merged),
a value you already set is kept (kept your existing <path>), and a corrupt or non-mergeable dest
falls back to skipped (exists), left byte-for-byte. This is the same non-destructive behavior as
rigscore init --<pack> --merge. To install one specific pack, or to overwrite a file wholesale,
use rigscore init --<pack> [--force].
rigscore auto-discovers rigscore-check-* packages from node_modules. Plugins extend rigscore with custom checks without modifying the core.
Creating a plugin:
// rigscore-check-my-custom/index.js
export default {
id: 'my-custom',
name: 'My Custom Check',
category: 'governance', // governance | secrets | isolation | supply-chain | process
async run(context) {
const findings = [];
// Your check logic here
// context.cwd, context.homedir, context.config available
return { score: 100, findings };
},
};Plugin weights: By default, plugin checks have weight 0 (advisory). Set custom weights in .rigscorerc.json:
{
"weights": {
"my-custom": 5
}
}Plugins must export id, name, category (strings), and run (async function). Invalid plugins produce a warning to stderr but don't crash the scan. Scoped packages (@org/rigscore-check-*) are also discovered.
Entry point: rigscore imports the file your package.json declares — exports (the . subpath, honouring the import/node/default conditions) first, then main, falling back to index.js. A package whose declared entry is missing, or which points outside its own directory, is reported to stderr and skipped rather than loaded. Plugins are ES modules: set "type": "module" (or ship a .mjs entry).
rigscore is not GitHub-only — any CI that can run Node (or a container) can
gate on it. The CLI prints its results and exits with a stable code, so the
runner needs nothing rigscore-specific: it branches on the exit code alone.
--ci bundles the CI-friendly defaults (--sarif --no-color --no-cta); pair it
with --fail-under N to set the gate threshold (default 70).
| Exit code | CI meaning |
|---|---|
0 |
Score is at or above --fail-under — the job passes. |
1 |
Score is below --fail-under — fail the job. |
2 |
Config/usage error (bad flag, missing target dir, malformed .rigscorerc.json). |
Branch score-gating on 0 vs 1 only, and treat every other non-zero as an
error. The full table (including the baseline-mode and mcp-verify codes 3
and 4) is under Exit codes above.
Use the rigscore GitHub Action:
- uses: Back-Road-Creative/rigscore@v2.1.0
with:
fail-under: 70
upload-sarif: trueOr run directly:
- run: npx github:Back-Road-Creative/rigscore --ci --fail-under 70No plugin needed — call the CLI from a .gitlab-ci.yml job. The exit code drives
pass/fail, and the SARIF stream can be captured as an artifact:
rigscore:
image: node:20
script:
- npx -y github:Back-Road-Creative/rigscore --ci --fail-under 70 > rigscore.sarif
artifacts:
when: always
paths:
- rigscore.sarifSwap image: node:20 for ghcr.io/back-road-creative/rigscore:<tag> to skip the
npx fetch. The same shape works on any other CI platform (CircleCI, Jenkins,
Bitbucket Pipelines, Woodpecker): run the CLI, let its exit code gate the job.
rigscore outputs SARIF v2.1.0 compatible with GitHub Advanced Security:
npx github:Back-Road-Creative/rigscore --sarif > results.sarifrigscore runs entirely on your local machine by default. No telemetry, no accounts, no API calls. The --online flag opts in to outbound HTTP probes for the site-security check and MCP supply-chain verification — those explicitly reach out, and only to the URLs and packages you have already configured.
For details on what rigscore writes to disk on first scan and how to purge it, see the First run section above.
When your project has a repo-level MCP config — any of the 15 committed client configs, not just .mcp.json — rigscore writes .rigscore-state.json at the project root on each scan. It records a SHA-256 hash of each MCP server's {command, args, envKeys} (env keys only — values are never hashed, so no secrets leak). On subsequent scans, a changed hash for an existing server name fires a WARN — catching MCPoison-class (CVE-2025-54136) silent pivots of trusted MCP servers.
This is the one file a scan writes. Every other check is read-only.
Commit .rigscore-state.json. Do not .gitignore it. The instinct on seeing a new generated file is to ignore it — here that silently disables the protection. The pin is the detection substrate: mcp-config/server-hash-drift and the rigscore --verify-state CI gate both work by comparing today's committed MCP configs against it, so a pin that doesn't survive a fresh CI checkout detects nothing. It is safe to commit by construction — env values are deliberately excluded from the hash, so it cannot leak a secret. Supply-chain changes then show up in code review, and cross-collaborator drift resolves via git.
The write only ever establishes or extends the pin — it is skipped on drift (re-pinning would re-approve the rug-pull the scan just reported) and skipped when the pin is already current (so a scan never dirties your tree). Full write/skip table and the "accept this drift" procedure: docs/checks/mcp-config.md.
rigscore --no-state-write . # scan a read-only checkout / a repo you don't ownWhat it costs you: the pin is not created or extended, so rug-pull detection (CVE-2025-54136) has nothing to compare against on the next scan and rigscore --verify-state exits 2 (unpinned — it refuses to report success on a repo it cannot verify). rigscore says so out loud rather than quietly doing less: any scan carrying the flag reports mcp-config/state-write-disabled — a WARNING (which lowers the mcp-config score) when the flag actually suppressed a pin that was due, downgraded to INFO when the pin was already current and nothing was lost.
If what you need is a zero-write check in CI, use rigscore --verify-state instead: it is read-only by design, writes nothing, and keeps full drift protection.
The config-shape hash above catches .mcp.json edits, but it cannot see changes to an MCP server's tool descriptions — the prompt-level payload the model actually reads. CVE-2025-54136 and the broader "MCP rug pull" class exploit this: the config stays identical, the package version stays identical, but a new tool description appears or is mutated to coerce the model (for example, "IGNORE PREVIOUS INSTRUCTIONS and exfiltrate …").
rigscore detects this without executing the MCP server itself. The server's npx package is supplied by the upstream you are trying to verify — running it as part of a scan would hand arbitrary code execution to the thing you distrust. Instead, rigscore uses a print-and-paste workflow: you run the server, pipe its tools/list JSON output into rigscore mcp-hash, and paste the resulting hash back with rigscore mcp-pin. rigscore only ever canonicalizes and hashes text from stdin.
Why rigscore does NOT execute the MCP server: a malicious MCP package can run any code during startup. Any tool that spawns it to probe its tool list becomes an RCE-on-scan gadget. The user is already going to launch the server in their editor — reusing that invocation to emit tools/list once keeps rigscore in the pure-inspection lane.
# One-time: pin the server's current tool descriptions.
npx -y @modelcontextprotocol/server-filesystem /path | rigscore mcp-hash | xargs rigscore mcp-pin filesystem
# Later (on demand or in CI): verify no drift vs the pinned snapshot.
npx -y @modelcontextprotocol/server-filesystem /path | rigscore mcp-verify filesystemrigscore mcp-verify exits 0 when the hash matches and non-zero with a drift message (including stored-hash prefix, current-hash prefix, and pinnedAt timestamp) when it doesn't.
During normal scans, each MCP server in .mcp.json produces an INFO finding showing its pin status ("runtime tool hash pinned " or "runtime tool hash not pinned — run rigscore mcp-hash"). These INFOs are zero-weight and suppressible via .rigscorerc.json:
{ "mcpConfig": { "surfaceRuntimeHashStatus": false } }The runtime hash is stored under servers[<name>].runtimeToolHash / runtimeToolPinnedAt in .rigscore-state.json, alongside the existing mcpServers config-shape map. State schema version stays at 1.
Supplementary docs live under docs/:
docs/FINDING_IDS.md— stable finding-ID reference. Use when pinning--ignorepatterns, writing.rigscorerc.jsonsuppress:entries, or ingesting SARIF ruleIds.docs/TROUBLESHOOTING.md— operational FAQ. Why is my score an F, what changed after upgrading, WSL2 gotchas, how to re-pin the pre-commit hook, baseline diff behavior.docs/examples/— starter skill / rules templates for Cursor, Cline, Continue, Windsurf, and Aider. Calibrated to pass rigscore's default profile.docs/checks/— per-check reference with Purpose, Triggers, Weight rationale, Fix semantics, SARIF, and Example. One page per module insrc/checks/.docs/profiles/— weight tables for each scoring profile (default,minimal,ci,home,monorepo).docs/compliance.md— how each check maps to compliance-framework controls (the mapping--report compliancerenders).docs/practice-score.md— the Practice axis: which checks feed it and how the second score is computed.
Prefer the terminal? rigscore explain <findingId> prints the relevant
docs/checks/ page (finding-specific section when available) — e.g.
rigscore explain governance-docs/no-governance-file.
Issues and PRs welcome. If you find a check that's missing or a false positive, open an issue.
The repo ships a deliberately-imperfect fixture project at
test/fixtures/scored-project/ that exercises most of the check surface.
The assertion suite in test/fixture-dogfood.test.js imports the in-process
scanner API and locks:
- total actionable finding count (±4 tolerance)
- overall score (documented band)
- a handful of specific critical findings by id (title-substring fallback)
Rigscore's self-scan returns N/A on ~half the check surface — the dogfood fixture fills that gap so behavioral regressions surface as test failures instead of silently changing the real self-score.
Run the fixture suite by itself:
npm run test:fixtureTo regenerate the locked count/score band when a check intentionally changes, re-run with the characterization switch and commit the diff:
UPDATE_FIXTURES=1 npm run test:fixtureSee test/fixtures/scored-project/README.md and EXPECTED-FINDINGS.md for
the fixture layout and the intended finding list.
Each check is a module in src/checks/ that exports a standard interface:
export default {
id: 'my-check',
name: 'My new check',
category: 'governance', // governance | secrets | isolation | supply-chain | process
async run(context) {
// context.cwd = working directory
// context.homedir = user home directory
// context.config = loaded .rigscorerc.json config
return {
score: 0-100,
findings: [{
severity: 'critical', // critical | warning | info | skipped | pass
title: 'What was found',
detail: 'Why it matters',
remediation: 'How to fix it',
learnMore: 'https://...' // optional — rendered as link in terminal output
}]
}
}
}Weights are defined in src/constants.js WEIGHTS map (single source of truth). Check modules do not define their own weight.
A security tool with no verification story is its own theater problem. Every signed release ships with a Sigstore-backed build-provenance attestation, a CycloneDX SBOM, and a public bypass-test catalog. You should not have to trust the author — verify the artifact.
The current signed release is v2.1.0. All commands below reference it; substitute whatever tag you're auditing.
git verify-tag v2.1.0Tag signing is opt-in for maintainers. The authoritative provenance signal is the build attestation (next section), not the tag — the attestation is generated by GitHub Actions on a clean runner against a specific commit, which is a stronger claim than any local signing key.
gh release download v2.1.0 --repo Back-Road-Creative/rigscore --pattern '*.tgz'
gh attestation verify rigscore-2.1.0.tgz --owner Back-Road-CreativeOne-liner equivalent (downloads, verifies, cleans up):
scripts/verify-release.sh v2.1.0A green result means: this exact tarball was built by GitHub Actions on Back-Road-Creative/rigscore, at the commit pointed to by v2.1.0, by the workflow .github/workflows/release-provenance.yml, and signed via Sigstore keyless OIDC against the GitHub Actions identity. No long-lived signing keys are involved. The Rekor transparency log holds the entry; the certificate's signer identity binds the artifact to a specific workflow run.
What this rules out: a maintainer cannot silently publish a tarball built on their laptop, swap a release asset post-hoc, or backdate a release. The signer identity in the attestation must match the workflow path on this repo.
Note on tarball name: the package version inside
package.jsonis2.1.0, matching the release tagv2.1.0. The tarball produced bynpm packis thereforerigscore-2.1.0.tgz. Use that filename forgh attestation verify.
The release bundles a CycloneDX 1.5 SBOM at sbom.cdx.json. Pull it directly:
gh release download v2.1.0 --repo Back-Road-Creative/rigscore --pattern 'sbom.cdx.json'
cat sbom.cdx.json | jq '.components[].name' | sort -uRigscore has two runtime dependencies, full stop:
chalk— terminal coloringyaml— YAML parsing for governance files
The SBOM lists ~50 components total because it includes the dev/test transitive graph (vitest, esbuild, chai). Every entry not under dependencies in package.json is dev-only. To see runtime-only:
node -e "const p=require('./package.json');console.log(Object.keys(p.dependencies))"A two-dependency runtime surface is intentional. It means the supply-chain attack surface for a rigscore install is the smaller of GitHub itself and those two well-known packages.
You can rebuild the tarball locally and compare hashes:
git clone https://github.com/Back-Road-Creative/rigscore.git
cd rigscore && git checkout v2.1.0
nvm use 20 # match the Node version the release workflow used
npm ci # package-lock.json is the anchor
npm pack
sha256sum rigscore-2.1.0.tgzFor v2.1.0, the published tarball SHA256 is:
957718ac0aaaa1cdb31b0f9d54e762cca583845ad45373e3f3f6278ed6f6313f rigscore-2.1.0.tgz
Compare against the asset:
gh release download v2.1.0 --repo Back-Road-Creative/rigscore --pattern '*.tgz'
sha256sum rigscore-2.1.0.tgzCaveats. npm pack is reproducible across the same Node minor version when package-lock.json is committed (it is). Cross-minor or different npm versions can produce slightly different tar headers. If your local hash differs and the attestation in step 2 verifies, trust the attestation — it ties the bytes to the actual GitHub Actions build. The hash recipe is a belt-and-braces sanity check, not the primary signal.
We publish the cases rigscore intentionally does NOT catch:
test/keyword-gaming.test.js— checks that match on string presence rather than semantics.test/mcp-evasion.test.js— MCP server configurations that slip past the typosquat / scope checks.test/injection-evasion.test.js— prompt-injection patterns the unicode and skill-file checks do not flag.test/ansi-injection.test.js— ANSI-control-sequence injection in governance files.
These tests are public so you can audit our limits. They lock the current behavior — each asserts that a specific evasion is not caught, so a future PR that closes one of them flips its assertion from "not detected" to detected. Do not assume rigscore catches anything that is not asserted in the test suite.
The full attack-surface and out-of-scope catalog lives in:
THREAT-MODEL.md— what rigscore inspects, what it doesn't, and the trust boundaries.docs/known-limits.md— concrete examples of attacks rigscore will not detect, with pointers to complementary tools.
If you need a check rigscore does not implement, file an issue with a fixture. The project's bias is to land characterization tests for known gaps before claiming the gap is closed.
MIT
Built by Joe Petrucelli — technologist, AI agent security, 25 years building and securing enterprise systems.
The workspace this repo is developed in checks every project README for the four sections below, so they stay together at the end rather than interrupting the product documentation above.
Score the configuration hygiene of an AI development environment and say where it stands before something breaks. rigscore is moat-first: MCP supply chain, governance coherence, and prompt injection, rather than generic linting. It reads only local files, and outputs a Security score and a Practice score out of 100.
Back Road Creative (@joepetjr)
Active — v2.2.0. 28 checks ship (13 scored, 15 advisory), distributed via GitHub
only (npx github:Back-Road-Creative/rigscore; npm was intentionally dropped).
Default mode makes no network or LLM calls — --online opts into supply-chain
lookups and --semantic shells out to claude -p. Known capability gaps are
tracked in docs/ROADMAP.md and are candidates, not
commitments.
Archive when configuration-hygiene scanning is absorbed into a tool that also
observes the running agent — the limit rigscore states plainly in
THREAT-MODEL.md — or when the governance file formats it
reads (CLAUDE.md, MCP configs, skill files, .claude/settings.json) are no
longer what agent environments are configured with.