habit-hooks-ruby
The Ruby Habit Hooks plugin: wraps rubocop for
code-smell detection. Instead of a bare offence, each finding is
coached by listing the smell, guide for fixing, then a list of file locations.
This causes the agent to focus on fixing the smell rather than the metric.
── high-complexity (1 issue) ──
High cyclomatic complexity means one function makes too many decisions at once. The count is the symptom; tangled responsibilities are the cause.
**Untangle the decisions:**
1. Lift guards out first — turn precondition checks into early returns so the happy path stays flat. Much of the count is preconditions wrapped around the real work.
2. Change the shape of what remains: an `if`/`else` chain switching on one value is often a lookup table or polymorphism in disguise; a nested loop is often a filter/map pipeline.
3. If the branches are genuinely separate jobs, extract one function per branch, each named for the responsibility it handles.
Useful tip: describe each branch in one sentence. Two branches with the same sentence belong together; a branch you cannot name cleanly wants its own function.
**AVOID**: merging conditions with and/or, or rewriting branches as ternaries, just to lower the score — the decisions remain, only the counter moves. You are done when a first-time reader can hold the whole function in their head.
app/services/billing.rb:6
Setup
The steps are:
- Install the plugin.
- Enable it in your
.habit-hooks/config.toml. - Make sure you have rubocop installed.
- Make sure your
.rubocop.ymlenables the cops you want to see.
Install the plugin
uv tool install "habit-hooks[ruby]" # pip and pipx work too
Or let setup do it: with habit-hooks already installed, habit-hooks init in your
project detects ruby, names this plugin in .habit-hooks/config.toml, and offers to
run the install for you — see the Habit-Hooks README #install.
Enable the plugin
Installing a plugin does not switch it on — it has to be named in
plugins before habit-hooks runs it.
# .habit-hooks/config.toml
plugins = ["ruby", "generic"]
Keep generic in the list. Most of the smells this plugin reports are coached
by guides generic ships; without it they fall back to a short generic prompt.
Sensors: install rubocop
The ruby plugin supports the rubocop sensor. It can use any rubocop on your PATH, but it is best to point it at the one your project uses.
Point it at your project's own rubocop:
If your Gemfile pins rubocop, or your .rubocop.yml names an extension gem
(rubocop-rails, rubocop-rspec, rubocop-performance), generate binstubs:
bundle binstubs rubocop
Otherwise, if you have no rubocop yet, install one:
rubocop—gem install rubocop
How it finds rubocop
habit-hooks' rubocop detector searches your project's bin/ ahead of the
machine's PATH, so bin/rubocop is what it will run — under your bundle,
with your extension gems loaded.
Without this, habit-hooks runs whatever rubocop your PATH answers with, and
a config naming cops that rubocop cannot load is a hard error rather than a
lint result:
Error: `Rails/*` has been extracted to the `rubocop-rails` gem.
habit-hooks reports that as a failed run.
Configure your .rubocop.yml
The sensor runs rubocop and reads what comes back. RuboCop discovers your .rubocop.yml
exactly as it does when you run rubocop by hand. The --force-exclusion is the one flag
habit-hooks adds, so your AllCops: Exclude: keeps applying even though habit-hooks names files
explicitly.
Cops are mapped to canonical smells and are reported under it:
| Cop | Smell |
|---|---|
Metrics/ParameterLists |
too-many-parameters |
Metrics/MethodLength |
oversized-function |
Metrics/BlockLength |
oversized-block |
Metrics/CyclomaticComplexity |
high-complexity |
Metrics/PerceivedComplexity |
high-complexity |
Metrics/AbcSize |
high-complexity |
Metrics/BlockNesting |
deep-nesting |
Lint/UselessAssignment |
unused-variable |
Lint/SuppressedException |
swallowed-exception |
Lint/Syntax |
parse-error |
All other cops are forwarded under their own name by default.
Recommended: add coaches for cops as needed (see Customization).
Set the root uncoached key to ignore to drop these cops,
or enforce to fail the run on them
(config.md).
A starting .rubocop.yml
If you have no config yet, this turns on the structural cops this plugin maps and nothing else. It is a suggestion, not a default. Habit-hooks never writes it for you and never passes it to rubocop.
AllCops:
NewCops: enable
Exclude:
- 'db/schema.rb'
- 'vendor/**/*'
- 'tmp/**/*'
Metrics/ParameterLists:
Max: 4
Metrics/MethodLength:
Max: 20
Metrics/CyclomaticComplexity:
Max: 10
Metrics/BlockNesting:
Max: 3
Lint/UselessAssignment:
Enabled: true
Lint/SuppressedException:
Enabled: true
Customization
Ruby Habit Hooks is customizable without modifying the installed package. Project
files under .habit-hooks/ruby/ override the corresponding files shipped by the
Ruby plugin. Commit these overrides if they are intended to apply to the whole
project.
Add project-specific coaching
Replace any Ruby guide by creating a file with the same name under
.habit-hooks/ruby/guides/. For example,
.habit-hooks/ruby/guides/high-complexity.md replaces the default coaching for
the high-complexity smell while leaving all other guides unchanged.
An unmapped cop forwarded under its own name can use a custom filename through the project config:
[smells."Style/StringLiterals"]
guide = "style-string-literals.md"
Then add a guide at .habit-hooks/ruby/guides/style-string-literals.md. For example:
Use the quote style established by this project. preserve interpolation and readability.
{% for issue in issues -%}
{{ issue.details.file }}:{{ issue.details.line }}
{% endfor %}
Guides are Markdown Jinja templates. They can use smell and language, read
smell-level details, and loop over issues to show each offense. The loop in
the example above writes the file and line of each issue (required, otherwise the
agent will not know where the offense occurred).
Add a cop-to-smell mapping
Prefer mapping a cop to an existing general smell when the guidance fits. This lets the project benefit from shared coaching and keeps the smell vocabulary small. If the cop represents a smell that should be useful beyond your project, consider contributing the mapping, guide, and any needed vocabulary changes in a pull request to the Habit Hooks project.
The built-in cop mappings live in sensors/rubocop_report.py, in the
COP_SMELLS table.
To add a mapping for a project, copy that file to
.habit-hooks/ruby/sensors/rubocop_report.py and add an entry, for example:
COP_SMELLS = {
# existing mappings ...
"Style/StringLiterals": "project-style",
}
Because the sensor helper imports this file from its own directory, also copy
rubocop_sensor.py and rubocop.toml to the same override directory (.habit-hooks/ruby/sensors).
Keep the recipe in the TOML file the same; its ${dir} then points at the override and
loads your customized report module.
If the new mapping uses an existing smell, its existing guide and severity are used. For a new smell, add a guide and configure its severity as needed:
[smells.project-style]
severity = "suggested"
Then add .habit-hooks/ruby/guides/project-style.md.
Add or replace a sensor
A sensor is a TOML recipe under sensors/. To replace the RuboCop sensor,
override .habit-hooks/ruby/sensors/rubocop.toml; to add a separate sensor,
create a new recipe such as .habit-hooks/ruby/sensors/custom-check.toml and
add its name to the Ruby plugin's sensors list in
.habit-hooks/ruby/config.toml:
sensors = ["rubocop", "custom-check"]
The plugin config override is a complete replacement, so copy the shipped
Ruby config.toml and preserve its language, files, transformers, and
detectors entries when adding a sensor. Declare every external command the
sensor uses in detectors, and use ${detector:<name>} in its recipe when the
sensor invokes that command.
The sensor must print a JSON array of Habit Hooks findings. See the sensor interface for the finding shape and available recipe placeholders.
For example, a simple custom sensor returning a canned finding to demonstrate the customization.
- Create
.habit-hooks/ruby/sensors/custom-check.toml:
command = "${dir}/custom-check.sh"
- Then create the executable
.habit-hooks/ruby/sensors/custom-check.sh:
#!/bin/sh
printf '%s\n' '[{"smell":"custom-check","details":{},"issues":[{"key":"app/models/example.rb","details":{"file":"app/models/example.rb","line":1,"message":"Canned custom-check result","source":"custom-check"}}]}]'
The sensor's issue paths should be relative to the project, and its output must always be a JSON array of findings.
- Enable it by copying the entire
plugins/ruby/src/habit_hooks_ruby/config.tomlto.habit-hooks/ruby/config.tomland adding"custom-check"to thesensorslist in the plugin config override. for example:
sensors = ["rubocop", "custom-check"]
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file habit_hooks_ruby-1.5.0.tar.gz.
File metadata
- Download URL: habit_hooks_ruby-1.5.0.tar.gz
- Upload date:
- Size: 25.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
50ff3393bca89a3b8b094d97689f4a74424a668c5dc1416ae249d39920eac6c6
|
|
| MD5 |
c958de4c582572d66f4b4ea27ac42f53
|
|
| BLAKE2b-256 |
4214fba44e3dd7efdee3623b40a0ab6f5983cb6b2ba229d6987b0f18b8a59092
|
Provenance
The following attestation bundles were made for habit_hooks_ruby-1.5.0.tar.gz:
Publisher:
release.yml on habit-hooks/habit-hooks
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
habit_hooks_ruby-1.5.0.tar.gz -
Subject digest:
50ff3393bca89a3b8b094d97689f4a74424a668c5dc1416ae249d39920eac6c6 - Sigstore transparency entry: 2728264633
- Sigstore integration time:
-
Permalink:
habit-hooks/habit-hooks@c5239ccb7bba4576932e2993cde62fa7aa1754a5 -
Branch / Tag:
refs/tags/v1.5.0 - Owner: https://github.com/habit-hooks
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@c5239ccb7bba4576932e2993cde62fa7aa1754a5 -
Trigger Event:
push
-
Statement type:
File details
Details for the file habit_hooks_ruby-1.5.0-py3-none-any.whl.
File metadata
- Download URL: habit_hooks_ruby-1.5.0-py3-none-any.whl
- Upload date:
- Size: 13.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
24440cdf679a60a266f47251a7ed56320397d52235be7756006d479c43a10947
|
|
| MD5 |
7247ace47b1a530cf1ea4e84720b01a6
|
|
| BLAKE2b-256 |
d7ca721f4ad1d3d70a1c4633abb212a1e04d7d4e12ce84287416c7cec9442885
|
Provenance
The following attestation bundles were made for habit_hooks_ruby-1.5.0-py3-none-any.whl:
Publisher:
release.yml on habit-hooks/habit-hooks
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
habit_hooks_ruby-1.5.0-py3-none-any.whl -
Subject digest:
24440cdf679a60a266f47251a7ed56320397d52235be7756006d479c43a10947 - Sigstore transparency entry: 2728264966
- Sigstore integration time:
-
Permalink:
habit-hooks/habit-hooks@c5239ccb7bba4576932e2993cde62fa7aa1754a5 -
Branch / Tag:
refs/tags/v1.5.0 - Owner: https://github.com/habit-hooks
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@c5239ccb7bba4576932e2993cde62fa7aa1754a5 -
Trigger Event:
push
-
Statement type: