tidy-settings — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited tidy-settings (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Occasional housekeeping on your Claude permissions. Settings files accumulate cruft: the same permission added narrowly five times, references to skills/scripts that no longer exist, redundant entries that one user-level rule already covers, and the occasional risky Bash(curl:*) you wish you hadn't accepted.
This skill cleans the mechanical noise itself and surfaces the judgment-heavy items for your review.
settings.json — to make sure it's tidy and free of personal/local lines.Complementary to fewer-permission-prompts (which adds permissions); this cleans them.
Auto-applied (mechanical, safe):
Bash, Edit, Read, Write, WebFetch, WebSearch, Skill, Task, mcp__*), alphabetical within group..bak) written alongside each modified file.Reported for your judgment (never auto-removed):
Bash(rm:*), Bash(curl:*), WebFetch(*), Read(//...), unrestricted Edit/Write, etc.).Bash(~/path/...) or Bash(/abs/path/...) pointing at missing files; Skill(name) for skills not installed.:* entry in the same section of the same file (allow vs allow, deny vs deny, ask vs ask). The narrow one is technically redundant, but see "Subsumption trade-off" below before recommending which to remove.allow shadowed by a wider :* entry in deny (deny wins, so the allow is dead code).Claude Code evaluates in this order. Always state this when explaining a subsumption or conflict finding:
--permission-mode behavior (default = ask, acceptEdits = auto-edit, auto/bypassPermissions = auto-allow).When a narrow entry is subsumed by a wider :* entry in the same section, removing one vs the other is not equivalent:
That second case is what bit the user in early testing — removing Bash(gh api:*) from ask left a handful of narrow gh api … entries, but every other gh api invocation now hits the default rather than always-prompting.
Section by section:
When presenting subsumption findings, name the section, name the trade-off, and give a default recommendation rather than asking the user to guess.
/tidy-settings # Audit + auto-apply mechanical fixes + interactive triage
/tidy-settings --report # Report only, no writes (even for sort/dedupe)Settings come in three roles. Resolve all of them with the helper script — never inline ad-hoc shell for this (variable assignments and while read loops bypass the permission allowlist and trigger noisy prompts):
~/.claude/skills/tidy-settings/scripts/resolve-files.shOutput is ---<ROLE>--- sections, one existing file path per line:
---USER--- — ~/.claude/settings.json / settings.local.json (always checked).---CANONICAL--- — the settings the main worktree uses. The script avoidsgit rev-parse --show-toplevel (inside a worktree it points at the worktree's own checkout) and instead resolves the first git worktree list entry's .claude through any symlink.
---WORKTREE--- — every other worktree's own real .claude/settings.local.json (and trackedsettings.json). These accumulate permissions that are lost when the worktree is pruned.
#### Worktree topology (why canonical ≠ pwd)
In the multi-repo setup, the main worktree's .claude is a symlink to a sibling state dir (e.g. myrepo/.claude → claude-myrepo/), and worktrees live inside that state dir at claude-myrepo/worktrees/<name>/ — each a real checkout with its own real .claude/. So:
settings.local.json (the one that survives) is reached through the **mainworktree's** symlinked .claude — the script's ---CANONICAL--- section.
settings.local.json is a separate, gitignored file that vanishes on prune.Permissions you accepted while working there are promotion candidates (step 4).
settings.json is git-tracked, so worktree copies converge on commit — drift there is reportedbut lower-stakes than the local file.
If there are no sibling worktrees, this degrades to exactly the old user + project behavior.
python3 ~/.claude/skills/tidy-settings/scripts/tidy_settings.py --apply \
--canonical "<canon>/settings.json" --canonical "<canon>/settings.local.json" \
--worktree "<wt>/.claude/settings.local.json" --worktree "<wt>/.claude/settings.json" \
<all files including the canonical and worktree ones...>Substitute the literal paths from step 1's output (don't reference shell variables — state doesn't persist between Bash calls). Every path is still passed positionally (so it's sorted/deduped/audited once); --canonical and --worktree just tag which positional paths participate in the promotion diff. Pass each ---CANONICAL--- line under --canonical and each ---WORKTREE--- line under --worktree. Omit both flags (and --apply) for plain --report mode; with no sibling worktrees, omit them entirely.
Parse the JSON output. The shape is:
{
"files": [
{
"path": "...",
"exists": true,
"error": "read error: ... | JSON parse error: ...", // present only on failure
"applied": {
"changed": bool, "deduped": {...}, "backup": "..." | null,
"realpath": "...", // where the bytes actually went (symlink resolved)
"verified": bool, // re-read confirmed the write landed
"write_error": "..." // present only if the write itself failed
},
"report": {
"current": {"allow": [...], "deny": [...], "ask": [...]},
"allow_audit": {
"subsumed_candidates": [{"narrow": "...", "broad": "..."}],
"broken_refs": [{"entry": "...", "kind": "missing_file|missing_skill", "resolved": "..."}],
"risk_flags": [{"entry": "...", "severity": "high|medium", "reason": "..."}],
"syntax_errors": [{"entry": "...", "reason": "..."}]
},
"deny_audit": {...},
"ask_audit": {...}
}
}
],
"cross_file": {"duplicates": [{"entry": "...", "in": ["...", "..."]}]},
"worktree_promotions": [
{"from": "<wt>/.claude/settings.local.json", "into": "<canon>/settings.local.json",
"section": "allow", "entries": ["Bash(gh run watch *)"]}
]
}worktree_promotions lists entries present in a worktree file but absent from the canonical file of the same basename — these are exactly the permissions that disappear when the worktree is pruned.
Print a single short block per modified file, e.g.:
✓ ~/.claude/settings.json — sorted, dedupe removed 0, backup at settings.json.bak
✓ <canon>/settings.local.json — sorted, dedupe removed 2, verified, backup at settings.local.json.bak
✗ <canon>/settings.json — write FAILED (permission denied) — see remediation in step 5
- ~/.claude/settings.local.json — absent, skippedWhen applied.changed is true, append verified (or a write FAILED callout) from applied.verified / applied.write_error — never report a write as done without it.
Order findings by severity:
Bash(rm:*), Bash(*), WebFetch(*), root-level Read/Edit/Write).allow_shadowed_by_deny) — these are dead code; the allow entry will never fire.Skill(name:*) vs Skill(name) in particular, the :* variant is typically meaningless (Skills don't take args) — recommend removing the :* form regardless of section.~/.claude/... or ~/Code/<not-current-repo>/... → suggest promoting to user-level (~/.claude/settings.json) and removing from project files.Skill(landscape), mcp__playwright__*) → suggest user level.worktree_promotions entry is apermission living only in a worktree's settings; it's lost when that worktree is pruned. Default recommendation: promote into the canonical `into` file so it survives. Apply judgment though — a one-off Bash(...) you accepted for a throwaway experiment may be better left to die with the worktree. Batch with AskUserQuestion when several share an obvious intent (e.g. "Promote these 3 gh-related permissions from worktree zazzy into canonical settings?"). For tracked settings.json drift, note that committing the worktree's change is usually the cleaner fix than promoting — call it out rather than copying bytes.
Bash(curl:*) if you do a lot of web work).For each finding, present:
Group similar findings and use AskUserQuestion to batch decisions where it helps (e.g., "All 6 narrow Skill(<name>) entries are subsumed by Skill(<name>:*) variants — remove all narrow ones?").
For removals: use Edit on the target JSON file to delete the line, including the trailing comma when not last, or the preceding comma when last in the array.
For cross-file promotion (move entry from file A to file B):
Edit.allow array (insert before the closing ]).--apply --no-backup on file B to re-sort.For worktree → canonical promotion (a worktree_promotions entry the user approved): the entry stays in the worktree (it may still be in use there) and is added to the canonical file. Append it to the canonical file's matching section array, then re-run the script with --apply --no-backup on the canonical file to re-sort.
#### Verify persistence (required when any canonical/worktree file was written)
The canonical file is reached through a .claude symlink and is often mode 600, so a write can silently fail or land on the wrong inode. After all mutations, re-run the script with `--report` on every file you touched and confirm:
error (read error / JSON parse error).applied.write_error is absent and applied.verified is true. Useapplied.realpath to confirm the bytes went where you intended (through the symlink, not beside it).
report.current.<section>.If a write failed (write_error set or verified false), emit a loud ✗ FAILED line and do not claim success. Remediate:
readlink -f <main_wt>/.claude — confirm the symlink resolves to a real directory.ls -l <realpath> — check the file mode; a 600 file the agent can't write is the usual cause.applied.realpath rather than the symlinked path, thenre-verify.
End with a short summary:
Applied:
- sorted/deduped 2 files
- removed 6 subsumed Skill(<name>) entries from ~/.claude/settings.json
- removed Bash(~/.claude/skills/landscape/scripts/pr-status.sh) (missing file)
- promoted Skill(landscape) and Skill(next) to user level
- promoted Bash(gh run watch *) from worktree zazzy → canonical (verified persisted)
Still on your radar:
- Bash(curl:*) in <project> — flagged medium-risk, you said keep
- WebSearch in user file — syntax check passes, but verify intent
Backups: 2 .bak files written; delete when you've verified.deny or ask arrays beyond exact dedupe. Promote/remove from those only on explicit user request.mcp__server__tool entries can't be verified for existence without a live MCP probe — leave them alone unless the user flags one.WebSearch and other bare tool names (no parens) are valid permissions — they sort with their tool prefix..claude/ is a symlink to another git repo (e.g. claude-myrepo/), the underlying real path is what gets edited.into path), never the reverse — the worktree copy is the ephemeral one. Promoting adds to canonical and leaves the worktree entry in place (it may still be in active use).settings.local.json drift is high-stakes: it's gitignored and per-worktree, so it's gone on prune. settings.json is git-tracked — worktree drift there is reported, but committing the worktree's change is usually the right fix, not byte-copying it into canonical.600 mode mean writes can silently fail; only applied.verified == true (a successful re-read) counts as persisted.~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.