Rails security
Use when auditing a Rails app for SQL injection, XSS, CSRF, mass-assignment, or Gemfile.lock CVEs, or when reviewing only NEW security regressions in a PR vs base branch.From its SKILL.md
npx -y skills add tuannv14/claude-team-toolkit --skill rails-securityAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
2 things to look at
- 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
- runs commandsInstructs the agent to run 8 commands, including `gem install brakeman bundler-audit` and 7 more.
SKILL.md
6.2 KB, ~1.9k tokens by cl100k_base, as published. Nobody here has run it
/rails-security — Brakeman + bundler-audit
Combined Rails security scan. No credentials. Subcommands:
vulns, cves, audit, diff, ignore, update.
Overview
Combined Brakeman (static analysis) + bundler-audit (CVE) scan for Rails apps. diff mode is the killer feature: shows only NEW issues vs base branch via git worktree (non-destructive — never touches your working tree).
When to Use
- Pre-PR security gate (new SQL injection, XSS, CSRF, mass-assignment)
- Auditing
Gemfile.lockfor known CVEs - PR review: only see what THIS PR introduced, not pre-existing noise
- Adding ignored issues with documented reason for audit trail
When NOT to Use
- Non-Rails projects → Brakeman is Rails-specific
- Runtime / dynamic security testing → use OWASP ZAP, not static scanners
- Dependency updates not security-related → use Dependabot / Renovate
- Auditing infrastructure (Docker, k8s) → wrong scope
Dependencies
gem install brakeman bundler-audit
If a tool is missing, the relevant subcommand is skipped with a clear message.
Helpers
brakeman_cmd() {
if [ -f "${1:-.}/Gemfile" ] && grep -q "brakeman" "${1:-.}/Gemfile" 2>/dev/null; then
echo "bundle exec brakeman"
else
echo "brakeman"
fi
}
Dispatch
vulns [path] [--severity high|medium|low|all] — Brakeman scan
PATH_ARG="${1:-.}"; SEV="${SEV:-medium}"
mapfile -t CMD < <(brakeman_cmd "$PATH_ARG" | tr ' ' '\n')
"${CMD[@]}" -p "$PATH_ARG" -f json -o /tmp/brakeman.json --no-progress 2>/dev/null
jq -r --arg sev "$SEV" '
.warnings[] | select(
($sev=="all") or
($sev=="high" and .confidence=="High") or
($sev=="medium" and (.confidence=="High" or .confidence=="Medium")) or
($sev=="low" and true))
| "[\(.confidence)] \(.warning_type): \(.message)\n \(.file):\(.line)"
' /tmp/brakeman.json
jq -r '.warnings | group_by(.confidence) | .[] | "\(.[0].confidence): \(length)"' /tmp/brakeman.json
cves [path] — bundler-audit CVE scan
PROJ="${1:-.}"
[ -f "$PROJ/Gemfile.lock" ] || { echo "No Gemfile.lock" >&2; return 1; }
bundle-audit update --quiet 2>/dev/null || true
(cd "$PROJ" && bundle-audit check --format json) > /tmp/bundler-audit.json 2>/dev/null
jq -r '
.results[]? | "[\(.advisory.criticality // "unknown" | ascii_upcase)] \(.gem.name) \(.gem.version)
CVE: \(.advisory.cve // .advisory.id) Fix: upgrade to \(.advisory.patched_versions | join(", "))
\(.advisory.title)"
' /tmp/bundler-audit.json
audit [path] — both + pre-PR gate (exit non-zero on HIGH or any CVE)
"$0" vulns "$1"; echo ""; "$0" cves "$1"
HIGH_BR=$(jq -r '[.warnings[] | select(.confidence=="High")] | length' /tmp/brakeman.json 2>/dev/null || echo 0)
CVES=$(jq -r '(.results // []) | length' /tmp/bundler-audit.json 2>/dev/null || echo 0)
[ "$HIGH_BR" -gt 0 ] || [ "$CVES" -gt 0 ] && {
echo "GATE: $HIGH_BR HIGH Brakeman + $CVES CVEs"
return 1
}
diff [base-branch] — only NEW issues vs base (non-destructive)
Uses git worktree add — your working tree is NEVER modified.
BASE="${BASE:-main}"
WT="$(mktemp -d)/rails-security-base"
trap "git worktree remove --force $WT 2>/dev/null; rm -rf $WT" EXIT INT TERM
git worktree add --quiet "$WT" "$BASE" || { echo "Cannot worktree at $BASE" >&2; return 1; }
mapfile -t CMD < <(brakeman_cmd "." | tr ' ' '\n')
(cd "$WT" && "${CMD[@]}" -f json -o /tmp/brakeman-base.json --no-progress 2>/dev/null) || true
"${CMD[@]}" -f json -o /tmp/brakeman-head.json --no-progress 2>/dev/null
echo "=== NEW Brakeman ==="
jq -s '(.[0].warnings|map(.fingerprint)) as $b | .[1].warnings | map(select(.fingerprint as $f | $b | index($f) | not)) | .[] | "[\(.confidence)] \(.warning_type) — \(.file):\(.line)"' /tmp/brakeman-base.json /tmp/brakeman-head.json
(cd "$WT" && bundle-audit check --format json) > /tmp/ba-base.json 2>/dev/null || echo '{}' > /tmp/ba-base.json
bundle-audit check --format json > /tmp/ba-head.json 2>/dev/null || echo '{}' > /tmp/ba-head.json
echo ""; echo "=== NEW CVEs ==="
jq -s '(.[0].results//[]|map(.advisory.id)) as $b | (.[1].results//[]) | map(select(.advisory.id as $id | $b | index($id) | not)) | .[] | "\(.gem.name) \(.gem.version) — \(.advisory.cve // .advisory.id)"' /tmp/ba-base.json /tmp/ba-head.json
Why worktree, not stash: stash is destructive on Ctrl-C/OOM, swallows merge conflicts. Worktree creates isolated checkout in temp dir.
ignore brakeman <fingerprint> [note] / ignore cve <CVE-ID> [note]
# Brakeman:
mkdir -p config; [ -f config/brakeman.ignore ] || echo '{"ignored_warnings":[]}' > config/brakeman.ignore
jq --arg fp "$FP" --arg note "${NOTE:-no reason given}" \
'.ignored_warnings += [{fingerprint:$fp, note:$note}]' config/brakeman.ignore > config/brakeman.ignore.tmp \
&& mv config/brakeman.ignore.tmp config/brakeman.ignore
# CVE:
[ -f .bundler-audit.yml ] || echo "ignore: []" > .bundler-audit.yml
echo "Edit .bundler-audit.yml to add: $CVE_ID # ${NOTE:-add reason}"
ALWAYS require a reason — without it future maintainers can't audit decisions.
update — refresh advisory DB
bundle-audit update # hits GitHub; fallback: bundle-audit check --no-update
Common Mistakes
- Ignoring without a reason → future maintainers can't audit decisions
- Running scanner on full codebase every PR → use
diffmode for noise reduction - Stash-based diff instead of worktree → destructive on Ctrl-C / OOM
- Skipping
bundle-audit update→ stale CVE database misses recent vulns - Brakeman ignore by line number → use fingerprint (survives refactors)
- Treating Brakeman
Lowas noise → some are real, just rare paths
Safety
- Scan output reveals attack surface (file paths, gem versions). Don't paste raw output publicly.
brakeman.ignoreand.bundler-audit.ymlMUST be committed (audit trail of accepted-risk decisions).- Never bypass scanners in CI without an approved exception (PR comment + ignore entry with reason).
- Use
diffmode for PR review — only see NEW issues, drastically cuts noise.
What ships with it
Read from the repository
Just SKILL.md. No reference files, no scripts.
Gives 0 of the 12 instructions most vulnerability scanning skills give in ~1.9k tokens
Counted across 223 of the 238 authors here whose files we hold, read 2026-09-06
- Fix critical findings immediatelyin 21 of 223, across 14 files
- Fix high findings before productionin 18 of 223, across 12 files
- Check and install AgentShield before scanningin 18 of 223, across 12 files
- Scaffold a secure configuration with initin 16 of 223, across 10 files
- Add the AgentShield GitHub Action to CIin 16 of 223, across 10 files
- Run the three-agent opus pipeline for deeper analysisin 14 of 223, across 8 files
- Apply safe auto-fixes onlyin 14 of 223, across 8 files
- Use JSON output for CI/CD integrationin 13 of 223, across 7 files
- Filter findings with a minimum severityin 12 of 223, across 10 files
- Classify each finding by severityin 10 of 223
- Use parameterized queries for all database accessin 10 of 223, across 9 files
- Write tests before writing the rulein 9 of 223, across 4 files
Said here and by no other author read
- Run diff mode for PR review
- Update the advisory database before scanning
- Require a documented reason for every ignore
- Commit both ignore files to the repository
- Ignore Brakeman warnings by fingerprint, not line number
- Use git worktree, not stash, for base comparison
Grouped from the skills themselves: near-identical wordings counted once, and counted by distinct author, so one author publishing three of these counts once. Length counted with cl100k_base; the agent that loads this file may tokenize it differently.