Enforcing Your Ruby Style Guide on AI-Generated Code Thoughtbot engineers built a Claude Code hook that automatically runs RuboCop against any Ruby files an AI coding agent modifies, then gives the agent a chance to fix violations it can catch. The hook enforces team coding conventions automatically rather than relying on the agent to remember them, as part of a broader practice called harness engineering that uses tools and guardrails to improve AI output quality. The approach adds deterministic feedback to the non-deterministic nature of AI-generated code, catching mistakes that rules alone cannot prevent. As AI-assisted software development becomes more widely adopted, more of the Ruby code in our Rails apps is being written by agents. Each team has its own conventions for how that code should look and behave, and we want those conventions enforced automatically rather than relying on the agent to remember them on its own. This is part of a broader practice called harness engineering, using tools, guardrails, validators, and persistence to increase the probability that our agents produce the outcomes we want. A capable model is only part of the equation. The rest is everything we put around it, including the context it operates within, the rules it follows, and the checks that catch its mistakes. The concept of harness engineering in software development is still in its early stages and there aren’t many resources on how to implement an agent harness within the context of Rails applications. At thoughtbot, we’re experimenting with how to encode how we work into various tools and contexts in order to increase the quality of the AI output. This post walks through one specific piece of the harness we’ve been building. It’s a Claude Code hook that runs RuboCop against any Ruby files the agent touches, gives the agent a chance to fix what it can, and surfaces what it can’t. Rules as the First Layer rules-as-the-first-layer We recently released a set of Claude Code rules https://github.com/thoughtbot/guides/tree/main/rails/ai-rules designed to be dropped into a project’s .claude/ directory so that coding agents can follow thoughtbot’s Rails conventions when writing code. It aims to ensure that when coding agents generate or modify code in a Rails project, that they adhere to conventions like TDD, RESTful routes, and strong params. You can use this as a starting point to add information specific to your project and the coding agent will use and update it when doing work. Think of it as a living memory for your coding agent, keeping track of architectural decisions, edge cases, and team conventions. The rules and context in these files are the feedforward https://martinfowler.com/articles/harness-engineering.html FeedforwardAndFeedback / inferential https://martinfowler.com/articles/harness-engineering.html ComputationalVsInferential aspect of our user harness. They guide the agent before and during work so that it increases the odds of getting the job right the first time. A linter can flag a 250-line controller action that’s doing too much but it can’t tell you which of those lines belong in the model. That’s where the agent can really add value, and where a good set of rules makes the difference. But rules alone aren’t enough. A good set of rules and a detailed yet concise CLAUDE.md file can greatly increase the quality of the agent’s code, but because results are non-deterministic, it isn’t guaranteed that the agent won’t make mistakes. This is where adding a feedback https://martinfowler.com/articles/harness-engineering.html FeedforwardAndFeedback / computational https://martinfowler.com/articles/harness-engineering.html ComputationalVsInferential aspect to our user harness can empower agents to fix their own mistakes and produce the results we want with less and less hand-holding. The rest of this post focuses on one specific feedback loop, using a Claude Code hook to run RuboCop on the Ruby files the agent has touched, and giving it a chance to fix any violations. Claude Code Hooks for Deterministic Behavior claude-code-hooks-for-deterministic-behavior This aspect of the user harness gives us deterministic control over the output of the code by using hooks https://code.claude.com/docs/en/hooks-guide . Hooks are custom shell commands, LLM prompts, or HTTP endpoints we define that can run when certain events happen in Claude Code’s lifecycle. This way, we can enforce certain actions always run rather than hoping the agent decides to do them. Your custom hooks and Claude Code communicate with each other via stdin , stdout , stderr , and exit codes. When your custom hook is executed, Claude Code passes event-specific data as JSON to your script’s stdin . Then your script tells Claude Code what to do next by either writing to stdout or stderr with a specific exit code. These scripts can run linters or prevent the agent from taking destructive actions, for example. An exit code of 0 tells Claude Code to proceed with whatever action it was performing. For many events your script hooks into, an exit code of 2 with a stderr message is used by Claude Code as feedback. Claude Code will use this information to block whatever event triggered it and take corrective action. Enforcing Ruby Style Guide Adherence enforcing-ruby-style-guide-adherence Lets look at an example with Rubocop. You may already have a pre-commit hook that runs rubocop with the --autocorect flag to fix things that are considered safe to auto-fix like style linting rules. Having this in a pre-commit hook that’s shared across your team, ensures you have a last line of defense when shipping code. Depending on the plugins you use though, there may be errors that surface which require judgement and reasoning in order to fix. These are fixes you make manually and that sometimes require knowledge of the architecture and other parts of the codebase. Injecting Rubocop into an agent’s lifecycle in the form of a hook in addition to a pre-commit hook can increase the trustworthiness of the agent’s output. Violations come back to the agent immediately while the change is in working memory and the agent can fix them in the same turn. These include fixes of the more complicated errors that require knowledge of other parts of the codebase. Here’s a simplified setup to get this up and running on your project. In .claude/hooks/rubocop-gate.sh , we’ll add a script that runs Rubocop and instructs the agent on how to fix errors that may require some reasoning. bash /bin/bash set -uo pipefail INPUT=$ cat cd "$CLAUDE PROJECT DIR" Find Ruby files Claude added, modified, or newly created not yet tracked . ruby files { { git diff --name-only --diff-filter=AM HEAD -- ' .rb' ' .rake' 'Gemfile' 'Rakefile'; git ls-files --others --exclude-standard -- ' .rb' ' .rake'; } | sort -u } RUBY FILES=$ ruby files if -z "$RUBY FILES" ; then exit 0 fi Second stop attempt: Claude already got one chance to fix violations. Surface anything still broken, then let it stop. if "$ echo "$INPUT" | jq -r '.stop hook active' " = "true" ; then REMAINING=$ bundle exec rubocop --force-exclusion $RUBY FILES 2 &1 if $? -ne 0 ; then echo "RuboCop violations remain after one retry. Surfacing for review:" &2 echo "$REMAINING" &2 fi exit 0 fi OUTPUT=$ bundle exec rubocop --force-exclusion --autocorrect $RUBY FILES 2 &1 STATUS=$? if $STATUS -ne 0 ; then cat &2 <