agentleFS
bordenetbordenet/superpowers-plus/skills/engineering/codeowners-drift-audit/skill.md

codeowners-drift-audit

Skill8 starsChanged 6 months agoMITView on GitHub

Audit CODEOWNERS for drift: finds unowned files, dead rules (patterns matching zero files), and invalid owner references. Works with GitHub and GitLab remotes, and degrades to advisory-only output when neither CLI is authenticated. Run when CODEOWNERS is out of date, files lack coverage, or ownership is unclear.

The file

We split this skill into the rules it gives an agent

1 rule. About 0 are common, 0 are distinctive, and 1 is very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈0

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 1

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    Run the single script below via bash (enforced at runtime, see Failure Modes).

    line 40

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
skill.md

CODEOWNERS Drift Audit

Wrong skill? PR reviewer auto-assignment → not this skill (read-only audit only). Blast radius of a code change → blast-radius-check.

Source: superpowers-plus Part of: Engineering Rigor skill family

When to Use

  • CODEOWNERS hasn't been reviewed in a while and ownership may have drifted
  • Files or directories seem to lack a clear owner
  • A rule in CODEOWNERS may no longer match anything (renamed/deleted paths)
  • An owner reference (@user, @org/team) may be stale or misspelled

How to Run

Run the single script below via bash (enforced at runtime, see Failure Modes). Locates CODEOWNERS (.github/, then root, then docs/ -- GitHub's own order), finds unowned files, dead rules, and invalid owners. Stateless.

Exit code: exit 0 with all three === headers printed means complete. Anything else is an incomplete audit -- never read that as "no issues found."

Large-repo advisory: cap FILES_ARR post-build for a quick sample (e.g. FILES_ARR=("${FILES_ARR[@]:0:100}")) -- but this also caps Step 2 (Dead Rules); see the Failure Modes row before deleting a rule it flags.

#!/usr/bin/env bash
set -euo pipefail

# -- Step 0: Preconditions --
# Under zsh, matches_pattern()'s case-glob silently mismatches everything.
if [ -z "${BASH_VERSION:-}" ]; then
  echo "ERROR: this script requires bash -- re-run as 'bash <this-script>'" >&2; exit 1
fi
if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
  echo "ERROR: not inside a git working tree" >&2; exit 1
fi
# CODEOWNERS candidates and git ls-files are both cwd-relative -- root first.
cd "$(git rev-parse --show-toplevel)" || { echo "ERROR: cannot cd to repo root" >&2; exit 1; }

CODEOWNERS_PATH=""
for candidate in .github/CODEOWNERS CODEOWNERS docs/CODEOWNERS; do
  [ -f "$candidate" ] && { CODEOWNERS_PATH="$candidate"; break; }
done
if [ -z "$CODEOWNERS_PATH" ]; then
  echo "ERROR: no CODEOWNERS file found" >&2; exit 1
fi
if [ ! -r "$CODEOWNERS_PATH" ]; then
  echo "ERROR: CODEOWNERS found but not readable: $CODEOWNERS_PATH" >&2; exit 1
fi
echo "CODEOWNERS: $CODEOWNERS_PATH"

# Match gitignore-style; anchoring MUST be decided before "/" is stripped
# (see Failure Modes for the anchoring/globstar regressions this fixes).
matches_pattern() {
  local f="$1" pattern="$2"
  local anchored=0
  case "$pattern" in
    '**/'*) pattern="${pattern#\*\*/}" ;;
    *)
      case "${pattern%/}" in
        */*) anchored=1 ;;
      esac
      ;;
  esac
  pattern="${pattern#/}"
  pattern="${pattern%/}"
  # gitignore's "/**/ " also matches zero intervening dirs.
  local pattern2="${pattern//\/\*\*\//\/}"
  if [ "$anchored" -eq 1 ]; then
    # shellcheck disable=SC2254
    case "$f" in
      $pattern|$pattern/*|$pattern2|$pattern2/*) return 0 ;;
    esac
  else
    # shellcheck disable=SC2254
    case "$f" in
      $pattern|$pattern/*|*/$pattern|*/$pattern/*|$pattern2|$pattern2/*|*/$pattern2|*/$pattern2/*) return 0 ;;
    esac
  fi
  return 1
}

# Rules (GitLab [Section] headers excluded, trailing "# comment" text and any
# CRLF stripped so neither pollutes Step 3's owner-token field split below).
RULES=$(grep -Ev '^\s*(#|$|\[)' "$CODEOWNERS_PATH" 2>/dev/null | tr -d '\r' | sed -E 's/[[:space:]]*#.*$//' || true)
echo "Rules: $(echo "$RULES" | grep -c . || true)"

# NUL-delimited into an array: git ls-files -z is never C-quoted, unlike
# newline-delimited output (handles non-ASCII/backslash filenames correctly).
FILES_ARR=()
while IFS= read -r -d '' f; do
  FILES_ARR+=("$f")
done < <(git ls-files -z)
echo "Tracked files: ${#FILES_ARR[@]}"

# -- Step 1: Unowned files --
echo ""
echo "=== Unowned Files ==="
for f in "${FILES_ARR[@]+"${FILES_ARR[@]}"}"; do
  owned=0
  while IFS= read -r rule; do
    [ -z "$rule" ] && continue
    read -r pattern _ <<< "$rule"
    if matches_pattern "$f" "$pattern"; then owned=1; break; fi
  done <<RULES_EOF
$RULES
RULES_EOF
  if [ "$owned" -eq 0 ]; then echo "  UNOWNED: $f"; fi
done

# -- Step 2: Dead rules --
echo ""
echo "=== Dead Rules ==="
echo "$RULES" | while IFS= read -r rule; do
  [ -z "$rule" ] && continue
  read -r pattern _ <<< "$rule"
  matched=0
  for f in "${FILES_ARR[@]+"${FILES_ARR[@]}"}"; do
    if matches_pattern "$f" "$pattern"; then matched=1; break; fi
  done
  if [ "$matched" -eq 0 ]; then echo "  DEAD RULE: $rule"; fi
done

# -- Step 3: Owner validity (best-effort) --
echo ""
echo "=== Owner Validity ==="
# Check every remote, parsing the actual hostname (see Failure Modes).
IS_GITHUB=0
for remote_name in $(git remote 2>/dev/null); do
  remote_url=$(git remote get-url "$remote_name" 2>/dev/null) || continue
  remote_host=$(printf '%s\n' "$remote_url" | sed -E 's#^[a-zA-Z][a-zA-Z0-9+.-]*://##; s#^[^@/]*@##; s#[:/].*##')
  if [ "$remote_host" = "github.com" ]; then IS_GITHUB=1; break; fi
done

# gh only for github.com, glab only otherwise (prevents cross-API misvalidation).
HAS_GH=0
if [ "$IS_GITHUB" -eq 1 ] && command -v gh >/dev/null 2>&1; then
  if gh auth status >/dev/null 2>&1; then
    HAS_GH=1
  else
    echo "  ADVISORY: gh not authenticated -- owner check skipped"
  fi
fi

HAS_GLAB=0
if [ "$IS_GITHUB" -eq 0 ] && command -v glab >/dev/null 2>&1; then
  if glab auth status >/dev/null 2>&1; then
    HAS_GLAB=1
  else
    echo "  ADVISORY: glab not authenticated -- owner check skipped"
  fi
fi

if [ "$HAS_GH" -eq 0 ] && [ "$HAS_GLAB" -eq 0 ]; then
  echo "  ADVISORY: owner verification skipped -- no authenticated CLI matching this remote's type on PATH"
  echo "  (unowned-file and dead-rule results above are unaffected -- CLI is only needed for owner validity)"
else
  echo "$RULES" \
    | awk '{for(i=2;i<=NF;i++) print $i}' \
    | sort -u \
    | while IFS= read -r owner; do
        [ -z "$owner" ] && continue
        # Email-format: non-@ char before @ + dot after @ = email, not username
        case "$owner" in
          [!@]*@*.*) echo "  UNVERIFIABLE: $owner (email-format)"; continue ;;
        esac
        # No "@" and not email-shaped: not a valid CODEOWNERS owner reference
        case "$owner" in
          @*) : ;;
          *) echo "  UNVERIFIABLE: $owner (missing @ prefix)"; continue ;;
        esac
        slug="${owner#@}"
        if [ "$HAS_GH" -eq 1 ]; then
          # Distinguish team refs (@org/team) from user refs (@user)
          if echo "$slug" | grep -q '/'; then
            if gh api "orgs/${slug%%/*}/teams/${slug##*/}" --silent 2>/dev/null; then
              echo "  VALID: $owner"
            else
              echo "  NOT_FOUND: $owner"
            fi
          else
            if gh api "users/${slug}" --silent 2>/dev/null; then
              echo "  VALID: $owner"
            else
              echo "  NOT_FOUND: $owner"
            fi
          fi
        elif [ "$HAS_GLAB" -eq 1 ]; then
          # glab users?username= exits 0 even for empty results -- inspect body
          ubody=$(glab api "users?username=${slug}" 2>/dev/null || echo "[]")
          # URL-encode slash for namespaced group paths (e.g. myorg/myteam -> myorg%2Fmyteam)
          encoded_slug=$(printf '%s' "$slug" | sed 's|/|%2F|g')
          gbody=$(glab api "groups/${encoded_slug}" 2>/dev/null || echo "{}")
          if echo "$ubody" | grep -q '"username"' || echo "$gbody" | grep -q '"full_path"'; then
            echo "  VALID: $owner"
          else
            echo "  NOT_FOUND: $owner"
          fi
        else
          echo "  UNVERIFIABLE: $owner (no matching CLI for remote type)"
        fi
      done
fi
echo ""
echo "=== Done. Review UNOWNED/DEAD RULE/NOT_FOUND lines above. ==="

Companion Skills

  • blast-radius-check — Before modifying code that a drifted CODEOWNERS rule might miss
  • pre-commit-gate — Pre-commit checks independent of ownership review

Failure Modes

Top rows only -- see reference.md for the full table.

FailurePrevention
zsh silently mismatches every pattern; sh/dash fail differently but still non-zeroStep 0 checks $BASH_VERSION, refuses to run without it
Leading-slash pattern (/build/) loses root-anchoring if / is stripped before the anchor decisionanchored is decided from ${pattern%/} before the leading / is stripped
Leading **/, or mid-string /**/, doesn't match zero intervening dirs (case has no recursive-glob)Leading **/ special-cased to any-depth; mid-string gets a collapsed-to-/ candidate
.github/CODEOWNERS vs root, or a subdirectory invocation, silently scopes the search/file-list wrongSearch order .github/→root→docs/; Step 0 cds to repo root first
Non-GitHub/non-GitLab remote + authenticated glab gets a confident but meaningless resultDisclosed, unfixed (reference.md) -- don't trust Owner Validity here
Large repo: capping FILES_ARR also corrupts Step 2See "Large-repo advisory" -- never delete a flagged DEAD RULE from a capped run
Non-zero exit, or stopping before "=== Done."Never read as "no issues found" -- rerun

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.