You want a script to run before every git command and before every file write. In settings.json, you write the hook matcher the way you already write your permissions: Bash(git *)|Write. Claude Code accepts it without a word. The hook fires on writes, never on git.
A hook matcher is not a permission rule. As soon as it contains anything other than letters, digits, _, -, spaces, , and |, the hooks documentation reads it as a regular expression compared with the tool name alone. Bash(git *) matches no tool: the filter on the command belonged in the if field.
The neighbouring case is sneakier. Write|Edit(*.ts) is not even a valid regular expression, and the documentation does not say what Claude Code does with it. Replayed on October 17 on Claude Code 2.1.302, with a hook that creates a witness file: a hook on Write|Edit(*.ts) did not fire when Claude created a file with the Write tool, while the same hook on Write|Edit did. The safeguard you put on your TypeScript files guards nothing.
Here is a settings.json with those two hooks, plus a deny rule on Read($HOME/.aws/**). Of these three mistakes, deadweight 0.25 only reported the last one. Version 0.26, on the same files (real output, cuts marked [...]):
=== ERROR (1) ===
[35-hooks-matcher] PreToolUse[1] in '.claude/settings.json' has matcher 'Write|Edit(*.ts)',
which holds the permission rule 'Edit(*.ts)' and is not a valid regular expression. The
hooks page does not say what Claude Code does with one; measured once, on Claude Code
2.1.302, such a group fired on none of its tools. Write the matcher 'Write|Edit' and put
'Edit(*.ts)' in a handler's `if` field (hooks).
=== WARN (1) ===
[42-permissions-rule] deny rule 'Read($HOME/.aws/**)' in 'settings.json' names `$HOME`, which
is not expanded in a permission rule [...]. Write Read(~/.aws/**) (permissions).
=== NOTICE (3) ===
[35-hooks-matcher] PreToolUse[0] in '.claude/settings.json' lists 'Bash(git *)' in matcher
'Bash(git *)|Write', written as a permission rule. A matcher is compared with the tool
name only, so that alternative matches no tool; the others still fire. [...]
[...]$HOME is the other trap in the example: the variable is not expanded in a permission rule. Replayed on October 17 on 2.1.302, with the rule passed as a command-line option (--disallowedTools): a deny on Read($HOME/...) let the file be read, while the same deny written with ~/ blocked it. A rule written with $HOME protects nothing, and nothing tells you so.
Two weeks, eight versions
I introduced deadweight on September 30, then explained how it is built on October 1. Since then, eight versions have shipped, up to 0.27.0, released on October 17. Here is what they change for anyone using it.
Fewer false alarms. On October 8, an external review of the auditor surfaced a series of defects, each one submitted to a second reviewer tasked with refuting it; 139 were fixed in 0.23. On 19 private repositories, with identical files, warnings dropped from 87 to 74. Version 0.26 removes one more: a SKILL.md starting with a UTF-8 BOM was declared "without YAML front matter", although Claude Code reads it normally.
More ignored settings caught. The matchers and the $HOME above, but also a lowercase claude.md, which Claude Code does not load on Linux (measured on 2.1.291; macOS and Windows were not tested). Or TaskOutput, removed from the tools in 2.1.277: left alone in an agent's tools:, it makes the agent get refused; next to Read, it grants nothing. Another punctuation trap: @docs/b.md: or @docs/b.md, is not an import. Measured on Claude Code 2.1.302, in a single run: nothing loads when :, ,, ., ", ;, !, ? or ) follows the path, nor when (, ", ' or a lone backtick precedes the @. Version 0.27 reports it as a NOTICE: to load the file, write a space after the path; to only name it, put it in backticks. The other neighbours of the path were not probed.
Every alert says what it is worth. Since 0.23, the report shows next to each check its measured precision, its date and the number of repositories. For matchers: precision 1/1 (100 %, 1 repositories, too few to conclude). An alert verified on a single repository; the tool itself writes that this is too few. These measurements were labelled by the assistant that wrote the checks, not blind.
What the session loads outside the repository. A session also loads your personal skills, your plugins and your MCP servers. Since 0.25, deadweight --environment counts them by origin. On a project measured on October 9, on the first turn: 16,705 tokens came from the project, 5,243 from the account's skills and plugins, 4,648 from 17 MCP servers. By default, this is an estimate drawn from file sizes, and it ignores servers added with claude mcp add: their file also holds your secrets, so the tool refuses to open it. --measure measures for real, but it runs claude -p in the folder without the trust dialog: the project's hooks run, its MCP servers connect. Keep it for a folder you know.
The rest is hardly visible in use: 465 test cases and a daily watch on Claude Code releases, which publishes roughly one a day. What you get from it: lists aligned with Claude Code up to 2.1.302, and run_probes.py, which replays against your own claude the facts the tool claims as measured.
Why 0.27
These fixes add up. Imports followed across four hops exist only in 0.27, the matchers and the BOM only since 0.26, the $HOME since 0.24, the external review since 0.23. An installation left at 0.21 has none of it.
If you use the CI floor, one precaution: 0.27 changes the auditor, so a floor recorded with 0.26 can no longer be compared. Run --set-floor once more and commit floor.json alone, so that the history tells the tool change apart from your own fixes.
What it still does not do
It does not audit your personal configuration. Pointed at ~/.claude, it says so and checks nothing (it no longer claims, as it did before 0.24, that your personal skills are loaded nowhere). --environment counts what is there without judging it.
A broken @import is reported in CLAUDE.md and in the files it imports, up to the four hops Claude Code follows. The file at the fourth hop is loaded but its own imports are not; the auditor does not read its tokens. A @docs/NOTES with no extension loads too, and the auditor still does not read it. And the error on an invalid matcher rests on a single run, which the message states plainly.
Try it
claude plugin marketplace add e-xode/deadweight --scope project
claude plugin install deadweight@e-xode --scope projectIt is free, under the MIT license: https://github.com/e-xode/deadweight
I work independently on this kind of problem: christophe.bragard@e-xode.net.
Comments (0)