repo-audit
Repo Audit
Audit GitHub repo security settings and hygiene. Detect drift, report findings, optionally auto-fix.
Invocation
/repo-audit # audit all active repos (default org: skinnyandbald)
/repo-audit <repo> # audit one repo (e.g. skinnyandbald/cos-bot)
/repo-audit --fix # audit + auto-fix what's fixable
/repo-audit <org> # audit all active repos in a specific org
/repo-audit <repo> --fix # audit one repo + fix
Workflow
Step 0: Preflight -- Token Permission Check
Before running any checks, verify the token has sufficient permissions:
# Check token is valid
gh api /user
# Check repo scope by testing vulnerability alerts on first repo
gh api repos/{first-repo}/vulnerability-alerts
If either call fails, warn the user and skip security settings checks (dependabot-alerts, dependabot-autofixes). Report clearly:
WARNING: Token permission check failed. Ensure your token has `repo` scope.
Security settings checks will be skipped.
Continue with remaining checks (dependabot-config, semgrep-syntax, stale-prs, stale-branches, actions-pinned) that only require read access.
Step 1: Build Repo List
Single-repo mode: If a specific repo was provided (contains /), use that repo directly. Skip to Step 2.
Org mode: List all active repos for the org (default: skinnyandbald).
Active repos = non-archived repos with a push in the last 180 days.
CUTOFF=$(date -v-180d +%Y-%m-%dT%H:%M:%SZ) # macOS; use `date -d '-180 days'` on Linux
gh repo list {org} --limit 200 --json nameWithOwner,isArchived,pushedAt --no-archived \
--jq --arg cutoff "$CUTOFF" '.[] | select(.pushedAt > $cutoff) | .nameWithOwner'
Report the repo count before proceeding: "Found N active repos in {org}."
Step 2: Run All Checks
For each repo, run every check below. Collect all results into three buckets: Critical, Warning, Clean.
Rate limit awareness: If auditing 50+ repos, add a 1-second pause between repos (sleep 1).
Check Definitions
1. dependabot-alerts (Critical, Auto-fixable)
Verify vulnerability alerts are enabled at the repo level.
gh api repos/{repo}/vulnerability-alerts
# 204 = enabled, 404 = disabled
A 404 response means alerts are disabled.
2. dependabot-autofixes (High, Auto-fixable)
Verify automated security fixes are enabled.
gh api repos/{repo}/automated-security-fixes
# Check the response JSON -- `enabled` field
3. dependabot-config (High, Auto-fixable)
Verify .github/dependabot.yml exists in the repo.
gh api repos/{repo}/contents/.github/dependabot.yml
# 404 = missing
4. semgrep-syntax (High, Auto-fixable)
Scan all *.yml files under .github/workflows/ for any file that contains both semgrep ci and --config auto on the same line or in the same step.
# List workflow files
workflows=$(gh api repos/{repo}/contents/.github/workflows --jq '.[].name' 2>/dev/null)
# For each workflow file, fetch content and check
for wf in $workflows; do
content=$(gh api repos/{repo}/contents/.github/workflows/$wf --jq '.content' | base64 -D)
if echo "$content" | grep -q 'semgrep ci' && echo "$content" | grep -q '\-\-config auto'; then
# Flag this file
echo "FAIL: $wf contains semgrep ci with --config auto"
fi
done
5. stale-prs (Low, Report only)
Find open PRs older than 30 days.
gh pr list --repo {repo} --state open --json number,title,createdAt,author \
--jq '.[] | select(.createdAt < "'$(date -v-30d +%Y-%m-%dT%H:%M:%SZ)'")'
Report count and PR numbers/titles.
6. stale-branches (Low, Report only)
Find branches with tip commit older than 60 days, excluding:
- The default branch
dependabot/*,renovate/*,release/*prefixes- Any branch that has an open PR (source or target)
# Get all open PR source and target branches (to exclude them)
open_pr_branches=$(gh api repos/{repo}/pulls --jq '.[].head.ref, .[].base.ref' | sort -u)
default_branch=$(gh api repos/{repo} --jq '.default_branch')
gh api repos/{repo}/branches --paginate | jq -r '.[].name' | while read branch; do
# Skip default branch
[[ "$branch" == "$default_branch" ]] && continue
# Skip dependabot/renovate/release branches
[[ "$branch" =~ ^(dependabot|renovate|release)/ ]] && continue
# Skip branches with open PRs
echo "$open_pr_branches" | grep -qx "$branch" && continue
# URL-encode branch name to handle slashes and special chars
encoded_branch=$(python3 -c "import urllib.parse, sys; print(urllib.parse.quote(sys.argv[1], safe=''))" "$branch")
# Check tip commit date
committed_at=$(gh api "repos/{repo}/git/refs/heads/${encoded_branch}" --jq '.object.url' \
| xargs gh api --jq '.commit.committer.date')
# Flag if older than 60 days
cutoff_60d=$(date -v-60d +%Y-%m-%dT%H:%M:%SZ)
if [[ "$committed_at" < "$cutoff_60d" ]]; then
echo "STALE: $branch (last commit: $committed_at)"
fi
done
7. actions-pinned (High, Report only)
Scan workflow files for third-party GitHub Actions that are NOT pinned to a SHA.
A uses: line is flagged if ALL of these are true:
- The action owner is NOT
actions/*orgithub/*(first-party exclusions) - The ref after
@is NOT a 40-character hexadecimal SHA
This catches @main, @master, @v1, @v1.2.3, and any other non-SHA ref.
# For each workflow file, fetch and scan
for wf in $workflows; do
content=$(gh api repos/{repo}/contents/.github/workflows/$wf --jq '.content' | base64 -D)
# Find uses: lines with non-SHA refs for third-party actions
echo "$content" | grep -E '^\s*uses:\s*' | while read line; do
# Extract action ref (owner/repo@ref)
action=$(echo "$line" | sed 's/.*uses:\s*//' | tr -d '"' | tr -d "'")
owner=$(echo "$action" | cut -d'/' -f1)
ref=$(echo "$action" | sed 's/.*@//' | cut -d' ' -f1)
# Skip first-party actions
[[ "$owner" == "actions" || "$owner" == "github" ]] && continue
# Check if ref is a 40-char hex SHA
if ! echo "$ref" | grep -qE '^[0-9a-f]{40}$'; then
echo "UNPINNED: $wf uses $action"
fi
done
done
Step 3: Report
Format results as a markdown report with three sections:
## Repo Audit Results
### Critical Issues
| Repo | Check | Details |
|------|-------|---------|
| cos-bot | dependabot-alerts | Disabled |
### Warnings
| Repo | Check | Details |
|------|-------|---------|
| prompto | stale-prs | 3 PRs older than 30 days |
| dear-ben | actions-pinned | 2 unpinned actions in ci.yml |
### All Clear
dear-ben, SecondBrain, distil (12 repos clean)
**Summary:** 2 critical, 3 warnings, 12 clean
Map check severities to report sections:
- Critical Issues:
dependabot-alerts(Critical severity) - Warnings:
dependabot-autofixes,dependabot-config,semgrep-syntax,actions-pinned(High severity),stale-prs,stale-branches(Low severity) - All Clear: repos with zero findings
If --fix was NOT passed, remind the user: "Run /repo-audit --fix to auto-fix fixable issues."
Step 4: Auto-Fix (only with --fix flag)
If --fix was not passed, STOP here. Do not modify anything.
Auto-fixable checks fall into two categories:
Settings Fixes (API PUT, no git required)
dependabot-alerts:
gh api repos/{repo}/vulnerability-alerts --method PUT
dependabot-autofixes:
gh api repos/{repo}/automated-security-fixes --method PUT
Content Fixes (GitHub Contents API, no clone required)
dependabot-config -- create .github/dependabot.yml with the default template (see below).
Only push if the repo has a package.json (npm ecosystem) or .github/workflows/ (actions ecosystem). Skip repos with neither.
For new file creation, omit the sha field:
# Check for ecosystem markers before fixing
if ! gh api repos/{repo}/contents/package.json &>/dev/null && \
! gh api repos/{repo}/contents/.github/workflows &>/dev/null; then
echo "Skipping {repo}: no npm or actions ecosystem found."
continue
fi
# Write template to temp file
cat > /tmp/dependabot.yml << 'DEPBOT'
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 10
cooldown:
default-days: 7
semver-major-days: 14
semver-minor-days: 7
semver-patch-days: 3
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 5
cooldown:
default-days: 3
DEPBOT
gh api repos/{repo}/contents/.github/dependabot.yml \
--method PUT \
-f message="chore: add dependabot config" \
-f content="$(base64 < /tmp/dependabot.yml)"
semgrep-syntax -- update each offending workflow file to remove --config auto.
Scan ALL *.yml files under .github/workflows/ (not just security.yml).
# Get current file content and SHA
sha=$(gh api repos/{repo}/contents/.github/workflows/{file} --jq '.sha')
content=$(gh api repos/{repo}/contents/.github/workflows/{file} --jq '.content' | base64 -D)
# Remove --config auto from the content
fixed_content=$(echo "$content" | sed '/semgrep ci/s/--config auto//g')
# Write fixed content to temp file
echo "$fixed_content" > /tmp/fixed-workflow.yml
# Push fix via Contents API
gh api repos/{repo}/contents/.github/workflows/{file} \
--method PUT \
-f message="fix: remove --config auto from semgrep ci" \
-f content="$(base64 < /tmp/fixed-workflow.yml)" \
-f sha="$sha"
Default Dependabot Template
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 10
cooldown:
default-days: 7
semver-major-days: 14
semver-minor-days: 7
semver-patch-days: 3
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 5
cooldown:
default-days: 3
Fix Report
After applying fixes, report what was changed:
## Fixes Applied
| Repo | Check | Action |
|------|-------|--------|
| cos-bot | dependabot-alerts | Enabled via API |
| cos-bot | dependabot-config | Created .github/dependabot.yml via Contents API |
| prompto | semgrep-syntax | Removed --config auto from security.yml |
Constraints
- Never modify repo code. Only repo settings and config files under
.github/. - Read-only by default. Only apply fixes when the
--fixflag is passed. - Skip archived repos. The repo list query already filters these out.
- Org-agnostic. Works with any GitHub org. Default to
skinnyandbaldif none specified. - Rate limits. If auditing 50+ repos, add
sleep 1between repos to avoid hitting GitHub API rate limits. - macOS date commands. Use
date -v-Ndsyntax (macOS). On Linux, usedate -d '-N days'instead. - No cloning. All checks and fixes use the GitHub API directly. No
git cloneneeded. - Confirm before fixing. When
--fixis passed, show the audit report FIRST, then confirm with the user before applying fixes. List exactly what will be changed.
More from skinnyandbald/fish-skills
simplify-parallel
Run code simplification across entire codebase using parallel agents with automatic segmentation and coordination
38critic-review
Unified plan review — stack detection, Context7 staleness scan, multi-model counselors dispatch, and prioritized triage. Three modes: full pipeline (default), --dry-run (copyable prompt), --feedback (analyze external input).
35pr-resolution
Resolve PR review comments. Use when asked to fix PR feedback, given a PR URL, or on '/pr-resolution'.
34counselors
Fan out a prompt to multiple AI coding agents in parallel and synthesize their responses.
32capture-learning
Capture comprehensive problem-solving narratives from work sessions, documenting the journey of discovery
32ceo-briefing
Research any topic and produce a structured executive briefing optimized for rapid CEO decision-making.
30