Metadata-Version: 2.4
Name: bridle-audit
Version: 0.1.0
Summary: Audit your Claude Code config: dead hooks, duplicate levers, boot-token cost.
Author-email: Jeremy Renoult <jeremy.renoult.pro@gmail.com>
License-Expression: MIT
Project-URL: Homepage, https://github.com/trinity-organism/bridle-audit
Project-URL: Repository, https://github.com/trinity-organism/bridle-audit
Project-URL: Changelog, https://github.com/trinity-organism/bridle-audit/blob/main/CHANGELOG.md
Project-URL: Issues, https://github.com/trinity-organism/bridle-audit/issues
Keywords: claude-code,claude,config,audit,hooks,cli
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Quality Assurance
Classifier: Topic :: Utilities
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Provides-Extra: dev
Requires-Dist: pytest; extra == "dev"
Dynamic: license-file

# bridle-audit

[![CI](https://github.com/trinity-organism/bridle-audit/actions/workflows/test.yml/badge.svg)](https://github.com/trinity-organism/bridle-audit/actions/workflows/test.yml)
[![PyPI](https://img.shields.io/pypi/v/bridle-audit)](https://pypi.org/project/bridle-audit/)
[![Python versions](https://img.shields.io/pypi/pyversions/bridle-audit)](https://pypi.org/project/bridle-audit/)
[![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)

Your Claude Code config grows in silence. Hooks pile up in `settings.json` —
some fire on **every single turn** — and keep firing long after the script
they point at was deleted. The same linter gets wired twice by two different
sessions. Skill descriptions are loaded into context at **every session
start**, and nobody counts what that boot costs. Configs only ever grow;
nothing prunes them.

`bridle-audit` is the pruner: **one command, zero dependencies, reads your
real config and tells you exactly what is dead, doubled, or dormant.**

- **Dead hooks** — commands wired to scripts that no longer exist (fired
  every turn, for nothing).
- **Duplicate levers** — the same script wired several times inside one event.
- **Dormant cost** — how many characters of skill descriptions you pay at
  every boot.

## Install

```
pipx install bridle-audit
```

or

```
pip install bridle-audit
```

or straight from source:

```
pipx install git+https://github.com/trinity-organism/bridle-audit
```

Python >= 3.9. Zero dependencies — standard library only.

## Usage

```
bridle-audit                 # audit $CLAUDE_CONFIG_DIR, falling back to ~/.claude
bridle-audit --config DIR    # audit a specific config directory
bridle-audit --json          # machine-readable output
```

Exit codes: `0` clean (or no config found), `1` at least one dead hook or
duplication, `2` config unreadable — so it drops straight into CI or a shell
prompt.

## Before / after

Real output (username anonymized). Before — a config that grew for six months:

```text
$ bridle-audit
bridle-audit — installed levers vs the ladder (context < skill < tool < hook < daemon)
hooks source: /Users/you/.claude/settings.json

[per session] SessionStart (1 hook)
  ok    session-banner.sh — /Users/you/.claude/hooks/session-banner.sh

[per turn] PreToolUse (2 hooks)
  ok    format-check.sh — /Users/you/.claude/hooks/format-check.sh
  ok    format-check.sh — /Users/you/.claude/hooks/format-check.sh
  DUP   format-check.sh x2 — same script, 2 voices in one event (one behavior, one lever, one place)

[per turn] Stop (1 hook)
  DEAD  notify.sh — /Users/you/.claude/hooks/notify.sh

dormant cost — 3 skills, ~414 chars of descriptions loaded at every session start

verdict — dead hooks: 1 · duplicate levers: 1 · skills: 3 (~414 chars/boot)
$ echo $?
1
```

After pruning the dead `Stop` hook and merging the duplicate into one matcher:

```text
$ bridle-audit
bridle-audit — installed levers vs the ladder (context < skill < tool < hook < daemon)
hooks source: /Users/you/.claude/settings.json

[per session] SessionStart (1 hook)
  ok    session-banner.sh — /Users/you/.claude/hooks/session-banner.sh

[per turn] PreToolUse (1 hook)
  ok    format-check.sh — /Users/you/.claude/hooks/format-check.sh

dormant cost — 3 skills, ~414 chars of descriptions loaded at every session start

verdict — dead hooks: 0 · duplicate levers: 0 · skills: 3 (~414 chars/boot)
$ echo $?
0
```

And when there is nothing to audit:

```text
$ bridle-audit --config /some/empty/dir
bridle-audit: no settings.json found in /some/empty/dir
Nothing to audit. If your Claude Code config lives elsewhere, point at it with --config DIR or $CLAUDE_CONFIG_DIR.
```

Hook commands are resolved the way a shell would: quoting (paths with
spaces), wrappers (`nohup`, `bash -c`, `python3`, ...), `VAR=val` prefixes
and `$HOME`/`~` are all handled before the target is checked.

## Philosophy

A bridle, not a harness rack: one behavior, one lever, one place — and always
the **cheapest lever that actually steers**. A line of context beats a skill,
a skill beats a tool, a tool beats a hook, a hook beats a daemon. This tool
shows you where your config climbed that ladder without need.

## Development

```
pip install -e ".[dev]"
pytest
```

The test suite fabricates synthetic configs in temp directories — it never
reads your real `~/.claude`.

## License

MIT
