Turn best-practice coding advice into AI habits. Stop reciting software engineering literature to your AI agent.
$ npx -y skills add habit-hooks/habit-hooks --agent claude-code
Turn best-practice coding advice into AI habits.
Stop reciting software engineering literature to your AI agent. Habit Hooks runs your linters, then replaces each raw rule violation with a short coaching guide the agent can act on — so it writes code like this:

👀 Looking for co-maintainers — see Contributing.
The effect: better code, better agent performance on the next task, and fewer tokens — good code needs less context to work in.
Does coaching actually beat a bare metric, or do both just get gamed? Liina Suoniemi measured it — independently, and pre-registered before any model ran.
function is too complex (11 > 5), Sonnet 4.6 produced a genuine
fix in 5.6% of trials. It gamed the check in 79 of 90 — splitting branches into helpers so the number fell
while the code got no simpler.18 real Python functions, one known smell each, 5 trials per function per condition, 90 trials per bar. Scored by a deterministic judge — no LLM — checked against blind human labelling at 90% agreement, Cohen's κ 0.85. Method, raw trials and the author's own list of what the judge can miss are all in the repository.
uv tool install habit-hooks # pip, pipx and brew work too
cd your-project
habit-hooks init
habit-hooks init detects your project's language, writes .habit-hooks/config.toml enabling the plugins it
needs, and lists everything still missing beside the command that installs it — offering to run them for you.
Re-run it any time: on a configured project it changes nothing and only reports what is missing, so it also
answers "why is this run not reporting anything?".
Setup is four steps. A run that reports nothing is almost always a skipped one:
.habit-hooks/config.toml. Installing one does not switch it on.jscpd, ruff, eslint and friends.Steps 3 and 4 are per project.
A Python package, requires Python 3.11+. macOS, Linux and native Windows — no WSL, no Git Bash, no shell of any kind:
uv tool install habit-hooks
# or
uvx habit-hooks
# or
pip install habit-hooks
# or
brew install habit-hooks/tap/habit-hooks
You get core plus the generic plugin, and four commands on your PATH: habit-hooks, habit-sensors,
habit-mapper, habit-snooze. Homebrew is the exception — it installs every plugin we ship, so skip to step 3.
⚠️ On its own this checks nothing about your language. The generic plugin measures file length and duplication. Python, TypeScript, PHP, Java and Ruby each need their own plugin — installed (step 2) and enabled (step 3).
The language plugins are opt-in via extras:
uv tool install "habit-hooks[typescript]" # one language
uv tool install "habit-hooks[python,typescript]" # several — name them in one command
uv tool install "habit-hooks[all]" # every language
⚠️ Each
uv tool installrebuilds the environment rather than adding to it, so a second one naming a different extra silently replaces the first: run[python]then[typescript]and you are left with typescript alone, and your Python project quietly stops being checked. Name every language in one command. (pip install "habit-hooks[python]"has no such trap — it adds.)
To pick plugins per project without a global install, run from the extra with uvx (uv caches it):
uvx --from "habit-hooks[typescript]" habit-hooks
Alternatively, vendor a plugin's files under .habit-hooks/<plugin>/ in your project. That works with any
install, because project files always override the installed package — including a plugin habit-hooks has no
package for.
Installing a plugin does not switch it on. However it got onto the machine, a plugin runs only once your
.habit-hooks/config.toml names it:
# .habit-hooks/config.toml
plugins = ["typescript", "generic"]
The list is ordered, and the order is a priority — see Plugins.
Detectors are not bundled: a plugin spawns the real tool, or reads it as a library.
| Plugin | Detectors | Install |
|---|---|---|
| generic | jscpd — the line counter is built in | npm install --save-dev jscpd |
| python | ruff, deptry | pip install ruff deptry |
| typescript | node, eslint, knip, ts-morph | npm install --save-dev eslint knip ts-morph (node from your system package manager) |
| php | php — phpmd ships bundled as a phar | nothing beyond a PHP runtime |
| java | pmd | brew install pmd |
| ruby | rubocop | gem install rubocop (or bundle binstubs rubocop) |
ts-morph is read as a library rather than spawned, so it belongs in your devDependencies — being on PATH
does nothing for it.
If your TypeScript project has no eslint config of its own, habit-hooks lints with the config it ships, which needs two more packages in your project:
npm install --save-dev @typescript-eslint/parser @typescript-eslint/eslint-plugin. A project with its owneslint.config.jsneeds neither — yours always wins.
If your Ruby project's
Gemfilepins rubocop, or its.rubocop.ymlnames an extension gem (rubocop-rails,rubocop-rspec), runbundle binstubs rubocop. habit-hooks looks inbin/first, sobin/rubocopruns under your bundle with those gems loaded. A rubocop that cannot load a gem your config names fails outright rather than linting, and habit-hooks reports that as a failed run.
habit-sensors prepends node_modules/.bin and .venv/bin to PATH, so a project's local tools are found
without being installed globally. A plugin whose tools live somewhere else names that directory on the
detector — bin is rubocop's, and only rubocop is looked for there.
habit-hooks init writes the smallest config that runs — the plugins, and nothing else assumed:
# .habit-hooks/config.toml
plugins = ["python", "generic"]
Naming no files is the recommended start: the run then scans what the active plugins declare, including
their exclusions — the python plugin already keeps .venv/ and site-packages/ out. Adding your own
files replaces those wholesale, exclusions and all.
Then run it:
habit-hooks
That scans every file in scope — on an existing codebase, that is the whole backlog on your first run.
Starting on an existing codebase? Snooze the backlog first. One command records today's findings as accepted, so from then on only new smells surface:
habit-sensors --all | habit-snooze --snooze
Commit the .habit-hooks/snooze.json it writes. Nothing is buried permanently — see
Snoozing existing violations, which also covers the ratchet that brings a
file's issues back the moment you touch it.
Scope the run explicitly instead — the flags are mutually exclusive:
habit-hooks --all # every file
habit-hooks --file src/billing.py # one file, ignoring snoozes
habit-hooks --branch main # files changed vs a base ref
habit-hooks --last 3 # files changed in the last 3 commits
habit-hooks --since <ref> # files changed since a commit
With no flag, the scope comes from [scope] in the config — which scans everything until you opt in. To make
a plain habit-hooks measure only your branch when you are not on mainBranch, set
[scope] autoBranchOffMain = true. On mainBranch itself it still scans everything.
A git-derived run measures what your branch changed since it left the base ref — from the merge base, so
files somebody else changed on the base afterwards are not yours to fix. Whatever picked the paths, files the
work tree no longer has are dropped, and the rest must match files. A base ref the checkout cannot resolve
fails the run rather than quietly scanning nothing.
The exit code separates a finding from a broken tool, so a CI wrapper can act on the difference:
| Exit | Meaning |
|---|---|
0 | clean — no enforced finding |
1 | an enforced finding — this branch has a smell to fix |
2 | the tool itself failed — a bad config key, an unresolvable base ref, a corrupt snooze index, or a plugin that is configured but not installed |
habit-hooks --version prints habit-hooks vX.Y.Z (likewise on the other three commands) — worth quoting in a
bug report, since the tool ships through four channels: PyPI, Homebrew, uvx and an npm shim.
When a change introduces a smell:
habit-hooks is a Claude Code plugin with 4 hand-picked skills, indexed on Flowy in the Skill Authoring category. Install it with the command on its page. It includes habit-hooks-prompting, habit-hooks-review, release-habit-hooks. Its skills do not fire on their own yet. Request auto-invocation to have Flowy route them as you prompt. Free and open source.
Is this plugin yours?
Claim it with GitHubSubmit a pluginPromote it