---
name: claude-hygiene
description: Use when auditing or fixing Claude Code config drift — settings permissions, hooks, skill staleness, plugin connectivity, memory age, context token usage.
triggers:
  - hygiene
  - claude hygiene
  - audit claude
  - claude audit
  - check settings
argument-hint: "[--fix|--clean] [--settings] [--skills] [--plugins] [--memory] [--tokens]"
---

# Claude Hygiene Audit

Audit all aspects of Claude Code setup. Surface drift, staleness, misconfiguration. Default: read-only. Pass `--fix` (or `--clean`) to auto-apply safe fixes via per-item confirmation using `AskUserQuestion`.

## Arguments

| Arg | Effect |
|-----|--------|
| (none) | Full audit, read-only report |
| `--fix` / `--clean` | Apply safe fixes — AskUserQuestion per item |
| `--settings` | Settings + hooks audit only |
| `--skills` | Skills audit only |
| `--plugins` | Plugins audit only |
| `--memory` | Memory audit only |
| `--tokens` | Token/context + plans/sessions report only |

---

## Output format note

Section headers and item lines below are format templates — Claude generates equivalent output, not verbatim echo. Each item: `[SEVERITY]  description — detail`.

---

## Auto-allowed Bash commands reference

NEVER worth putting in `permissions.allow` — Claude Code auto-allows these:

```
cat, head, tail, wc, stat, ls, cd, find, echo, printf, diff, grep, egrep, fgrep,
rg, sort, uniq, cut, paste, tr, jq, sed (read-only), file, which, type, date,
hostname, ps, df, du, tree, git status, git log, git diff, git show, git branch,
git tag, git remote, git ls-files, git stash list, git reflog, git worktree list,
gh pr view, gh pr list, gh pr diff, gh pr checks, gh issue view, gh issue list,
gh run view, gh run list, gh repo view, gh auth status,
docker ps, docker images, docker logs, docker inspect
```

**No-op match rule:** Strip `Bash(` wrapper and trailing ` *` / whitespace. Result is no-op if it exactly matches or starts-with-then-space-matches an auto-allowed command above.
- `Bash(git status *)` → strip → `git status` → exact match → **no-op**
- `Bash(git status --short *)` → strip → `git status --short` → starts with `git status ` → **no-op**
- `Bash(git commit *)` → strip → `git commit` → not in list → **not a no-op**

---

## Path computation

**Important:** Shell vars do NOT survive across Bash tool calls. Compute project path inline every time:

```bash
PROJECT_KEY=$(pwd | tr '/' '-')   # keep leading dash — Claude project dirs have it
PROJECT_MEM=~/.claude/projects/$PROJECT_KEY/memory
PROJECT_SESSIONS=~/.claude/projects/$PROJECT_KEY
```

---

## Workflow

### Step 1 — Determine scope

Check args. Specific flag passed → run only that section. Otherwise run all.

---

### Step 2 — Settings Audit (skip if not in scope)

```bash
cat ~/.claude/settings.json
cat .claude/settings.json 2>/dev/null || echo "(no project settings)"
```

Check each file for:

**A. No-op allow rules** — `permissions.allow` entries already auto-allowed (see reference above).

**B. Redundant ask/deny overlap** — entries in `permissions.ask` also in `permissions.deny`. Deny wins; ask entry dead.

**C. Bypass mode active** — `"defaultMode": "auto"` means `ask` rules silently skipped. Warn: "ask list provides no protection in bypass mode."

**D. Model version** — check `"model"` field. Warn if deprecated model ID (anything not `sonnet`, `opus`, `haiku`, or valid `claude-*` ID from current family).

**E. Ghost plugin entries** — `enabledPlugins` keys not in `~/.claude/plugins/installed_plugins.json`.

Output format:
```
### Settings
[OK]   deny rules: 14 entries, no duplicates
[WARN] allow: "Bash(git status *)" is auto-allowed — remove
[WARN] defaultMode: "auto" (bypass) — ask list has no effect
[OK]   model: sonnet
[WARN] enabledPlugins: "foo@bar" not in installed_plugins.json (ghost entry)
```

---

### Step 2b — Hooks Audit (runs as part of --settings)

```bash
# Extract all hook commands from global settings
cat ~/.claude/settings.json | jq -r '.hooks | to_entries[]? | "\(.key): \(.value[]?.hooks[]?.command // empty)"' 2>/dev/null

# Extract from project settings
cat .claude/settings.json 2>/dev/null | jq -r '.hooks | to_entries[]? | "\(.key): \(.value[]?.hooks[]?.command // empty)"' 2>/dev/null
```

For each hook command:
- Extract leading binary: `awk '{print $1}'` on command string
- Check binary exists: `which <binary> 2>/dev/null`
- If command starts with `/` or `~/` or `$HOME/`: check file exists on disk

**A. Missing binary** — command references binary not in PATH. Hook silently fails.

**B. Missing script file** — hook points to absolute/home path that doesn't exist.

**C. Hardcoded home path** — hook uses `/Users/<name>/...`. Warn to use `$HOME` (breaks on other machines).

**D. MCP disconnected** — check current-turn `<system-reminder>` only. If MCP server appears disconnected, report it. Don't infer from prior turns.

Output format:
```
### Hooks
[OK]   PreToolUse: bash /Users/x/.claude/statusline-command.sh — file exists
[WARN] PostToolUse: /Users/x/.claude/missing-hook.sh — file not found
[WARN] hook binary "acli" not in PATH — hook will fail silently
[WARN] hardcoded path in PreToolUse hook — use $HOME instead of /Users/x
```

---

### Step 3 — Skills Audit (skip if not in scope)

```bash
ls -la ~/.claude/skills/
find ~/.claude/skills -name "SKILL.md" | sort
find ~/.claude/skills -type l | sort
```

For each skill directory:

**A. Missing SKILL.md** — directory exists, no skill file. Dead directory.

**B. Missing frontmatter fields** — check each SKILL.md for `name:` and `description:`. Missing description = skill won't trigger by description-matching.

**C. Description quality** — should start with "Use when" or "Use to" (explicit trigger phrases), under 250 chars. Flag if trigger language is missing.

**D. Namespacing and Prefixes** — check that the skill name uses correct namespace conventions to optimize name-only dynamic loading:
* `agent:<role>` (Persona-based agents)
* `error:<category>` (Error handling patterns)
* `syntax:<category>` (Language/framework syntax rules)
* `impl:<category>` (Implementation workflow instructions)
* `omc:<category>` (Oh My Claude dynamic learning skills)
* `tool:<name>` (Standalone scripts and formatting utilities)
If name matches a prefix directory (like `agents/` or `errors/`) but lacks the namespace prefix in the `name:` frontmatter key, flag as namespace drift.

**E. Staleness** — check mtime. Flag if >90 days unmodified (informational — old ≠ stale, but worth reviewing).

**F. Broken symlinks** — find symlinks pointing to missing targets:
```bash
find ~/.claude/skills -type l ! -exec test -e {} \; -print
```

**G. Duplicate names** — two SKILL.md files with same `name:` in frontmatter conflict.

**H. Wasted workflow skills** — check `settings.json` `skillOverrides`. Skill set to `name-only` but SKILL.md has >50 lines = content never loaded. Warn.

**I. Ghost skillOverrides** — for each non-plugin key in `skillOverrides` (no `:` in name = local skill), check if `~/.claude/skills/<name>/SKILL.md` exists. Plugin skills (`plugin:skill` format) OK to skip.

Also check `~/.claude/commands/` for same drift signals.

Output format:
```
### Skills (23 found)
[OK]   claude-hygiene — updated 0d ago
[WARN] skill "ceo" should use namespace "agent:ceo"
[WARN] skill "erpnext-errors-purchase" description missing "Use when/Use to" trigger phrase
[INFO] erpnext-errors-purchase — not modified in 142d (review if still accurate)
[WARN] broken-skill/ — SKILL.md missing
[WARN] symlink: generate-invoice -> ../omc-learned/generate-invoice (broken)
[WARN] skillOverride: humanizer set to name-only but has 120 lines of workflow
[WARN] skillOverride: "old-skill" references non-existent ~/.claude/skills/old-skill/
```

---

### Step 4 — Plugins Audit (skip if not in scope)

```bash
cat ~/.claude/plugins/installed_plugins.json
```

Cross-reference installed vs enabled (from `settings.json` `enabledPlugins`).

**A. Installed but not enabled** — plugin files exist, not in enabledPlugins. May be intentional or forgotten.

**B. Enabled but not installed** — ghost entry in enabledPlugins, no installed plugin backing it.

**C. Connectivity** — check current-turn `<system-reminder>` only for "MCP server disconnected" signals.

Output format:
```
### Plugins (9 installed, 5 enabled)
[OK]   atlassian@claude-plugins-official — installed, enabled
[OK]   slack@claude-plugins-official — installed, enabled
[INFO] github@claude-plugins-official — installed, NOT enabled
[WARN] ghost@example — in enabledPlugins but not installed
[WARN] mcp__claude_ai_Atlassian — disconnected this session (per system-reminder)
```

---

### Step 5 — Memory Audit (skip if not in scope)

```bash
# Inline project path computation — do NOT rely on vars from prior Bash calls
cat ~/.claude/projects/$(pwd | tr '/' '-')/memory/MEMORY.md 2>/dev/null || echo "(no project memory)"
ls -la ~/.claude/projects/$(pwd | tr '/' '-')/memory/ 2>/dev/null
```

For each memory file in MEMORY.md:

**A. Missing file** — MEMORY.md links to `.md` file not on disk.

**B. Orphan files** — `.md` files in memory dir NOT linked from MEMORY.md. Invisible to future sessions.

**C. Age** — check mtime. Flag if >30 days (project memories decay fast per memory guidelines).

**D. Incomplete structure** — for `feedback` or `project` type, check if body has `**Why:**` and `**How to apply:**`. Missing = harder to apply correctly.

**E. Missing frontmatter** — no `name:`, `description:`, or `type:` in frontmatter.

**F. Size** — report total count and disk size of all memory files.

Output format:
```
### Memory (8 files, 24KB)
[OK]   feedback_testing.md — updated 3d ago
[WARN] project_auth_rewrite.md — 45d old (project memory likely stale)
[WARN] feedback_tdd.md — missing **Why:** section
[WARN] MEMORY.md links to reference_jira.md — file not found on disk
[WARN] orphan: old_note.md — on disk but not in MEMORY.md index
```

---

### Step 6 — Token / Context + Plans / Sessions Report (skip if not in scope)

```bash
which rtk 2>/dev/null && rtk gain --history 2>/dev/null || echo "(rtk not available — skipping cache report)"

# Session sizes (inline project path)
ls -lh ~/.claude/projects/$(pwd | tr '/' '-')/*.jsonl 2>/dev/null | sort -k5 -rh | head -5
for f in ~/.claude/projects/$(pwd | tr '/' '-')/*.jsonl 2>/dev/null; do wc -l "$f"; done | sort -rn | head -5

# Total projects dir disk usage
du -sh ~/.claude/projects/ 2>/dev/null

# Subagent session traces (nested under session dirs)
find ~/.claude/projects/$(pwd | tr '/' '-') -path '*/subagents/*.jsonl' 2>/dev/null | xargs ls -lh 2>/dev/null | sort -k5 -rh | head -5

# Stale plan files
find ~/.claude/plans -name "*.md" -mtime +14 2>/dev/null | sort
```

**A. RTK/cache status** — if rtk available, show savings. Skip silently if not.

**B. Session size** — largest JSONL = longest sessions. Flag if any >5000 lines.

**C. Stale plans** — plan files older than 14 days. Completed plans should be deleted.

**D. Disk usage** — total `~/.claude/projects/` size. Flag if >500MB.

**E. Recommendations** — current session large → suggest `/compact`. Many large sessions → suggest reviewing `claudeMdExcludes`. Stale plans → offer delete in `--fix` mode.

Output format:
```
### Tokens / Disk
[OK]   RTK active — 68% savings (last 7d)
[INFO] 5 sessions, largest: c44fb4e7.jsonl (3,201 lines / 2.1MB)
[WARN] current session: 8,440 lines — consider /compact
[INFO] ~/.claude/projects/: 340MB total
[WARN] stale plans: feature-auth-flow.md (23d), old-bug.md (45d) — delete?
```

---

### Step 7 — Summary

Print combined summary with counts, then prioritized action items.

**Priority order:** `[ERROR]` first, then `[WARN]`, then `[INFO]`. Within same severity: security/bypass first, broken hooks second, missing files third, staleness last.

```
## Claude Hygiene Report — <today's date>

Settings   [2 warnings]
Hooks      [1 warning]
Skills     [1 error, 3 info]
Plugins    [1 warning]
Memory     [2 warnings]
Tokens     [1 warning]

## Action Items (errors → warnings → info)
1. [WARN] settings/global: "Bash(git status *)" in allow — already auto-allowed, remove
2. [WARN] hooks: /Users/x/.claude/missing-hook.sh — file not found, hook silently fails
3. [WARN] memory: project_auth_rewrite.md — 45d old. Review and update or delete.
4. [WARN] plugin: mcp__claude_ai_Atlassian disconnected this session
5. [INFO] skills: erpnext-errors-purchase — 142d old. Still accurate?
6. [INFO] tokens: current session 8,440 lines — run /compact if context feels slow
```

---

### Step 8 — Apply fixes (only if `--fix` or `--clean` passed)

**Use `AskUserQuestion` per fixable item — not shell stdin.** Never batch-apply. One question per fix, or group homogeneous items into `multiSelect` (e.g., multiple stale plans). `AskUserQuestion` caps at 4 questions and 4 options — paginate if >4 items in category.

Example:
> Apply fix: Remove no-op allow rule "Bash(git status *)" from ~/.claude/settings.json?

After all confirmations:
```
Applied 2 fixes. Skipped 1.
```

**Fixable automatically:**
- Remove no-op allow rules
- Remove ghost enabledPlugins entries
- Delete stale plan files (>14 days, per-file confirm)

**Never auto-fix:**
- deny/ask rules — require human judgment
- skill content — require human review
- plugin enable/disable state
- memory files — report only; user deletes manually
- hook commands — paths may be intentional

---

## Rules

- Default run strictly read-only. No writes unless `--fix`/`--clean` passed.
- Never delete memory files — report only; user decides.
- Never modify deny rules automatically.
- Never batch-apply fixes — one `AskUserQuestion` per item (or multiSelect for homogeneous groups).
- Compute project paths inline every Bash call — shell vars don't survive.
- RTK not installed → skip RTK section without error.
- Keep report scannable: one line per issue, severity prefix `[OK]`/`[INFO]`/`[WARN]`/`[ERROR]`.
- MCP connectivity checks use current-turn `<system-reminder>` only.