Skip to main content

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:

  1. Install the plugin.
  2. Enable it in your .habit-hooks/config.toml.
  3. Make sure you have rubocop installed.
  4. Make sure your .rubocop.yml enables 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:

  • rubocopgem 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.toml to .habit-hooks/ruby/config.toml and adding "custom-check" to the sensors list 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

habit_hooks_ruby-1.5.0.tar.gz (25.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

habit_hooks_ruby-1.5.0-py3-none-any.whl (13.7 kB view details)

Uploaded Python 3

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

Hashes for habit_hooks_ruby-1.5.0.tar.gz
Algorithm Hash digest
SHA256 50ff3393bca89a3b8b094d97689f4a74424a668c5dc1416ae249d39920eac6c6
MD5 c958de4c582572d66f4b4ea27ac42f53
BLAKE2b-256 4214fba44e3dd7efdee3623b40a0ab6f5983cb6b2ba229d6987b0f18b8a59092

See more details on using hashes here.

Provenance

The following attestation bundles were made for habit_hooks_ruby-1.5.0.tar.gz:

Publisher: release.yml on habit-hooks/habit-hooks

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file habit_hooks_ruby-1.5.0-py3-none-any.whl.

File metadata

File hashes

Hashes for habit_hooks_ruby-1.5.0-py3-none-any.whl
Algorithm Hash digest
SHA256 24440cdf679a60a266f47251a7ed56320397d52235be7756006d479c43a10947
MD5 7247ace47b1a530cf1ea4e84720b01a6
BLAKE2b-256 d7ca721f4ad1d3d70a1c4633abb212a1e04d7d4e12ce84287416c7cec9442885

See more details on using hashes here.

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

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

This release

1.5.0 This release

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page