The single most important thing that made me believe AI coding could work
When I started coding with AI, my single most important frustration was when Claude wasn't following instructions.
I knew exactly what I wanted: we're (when I'm referring to "we" I mean Claude and I) adding a new endpoint to expose a new model. The getter for index is a bit complicated, it requires a few joins. I knew what I wanted, instructions were clear.
I'm getting this monster (dramatized):
def index
@reviewer_membership = Membership.find(params[:membership_id])
task_ids = []
task_ids << @reviewer_membership.current_task.id if @reviewer_membership.current_task.present?
@reviewer_membership.completed_tasks.each do |t|
task_ids << t.id
end
@reviews = CodeReview.where("code_reviews.id IN (
SELECT cr.id FROM code_reviews cr
INNER JOIN projects p ON p.id = cr.project_id
INNER JOIN peer_reviews pr ON pr.code_review_id = cr.id
WHERE p.id = " + @reviewer_membership.project.id.to_s + "
AND cr.membership_id != " + @reviewer_membership.id.to_s + "
AND pr.status = 'requested'
)")
if task_ids.length > 0
@reviews = @reviews.where("code_reviews.task_id IN (" + task_ids.join(",") + ")")
else
@reviews = @reviews.where("1=0")
end
@reviews = @reviews.joins("LEFT OUTER JOIN tasks ON tasks.id = code_reviews.task_id")
@reviews = @reviews.joins("LEFT OUTER JOIN sprints ON sprints.id = tasks.sprint_id")
@reviews = @reviews.select("code_reviews.*, tasks.name as task_name, sprints.name as sprint_name")
@reviews = @reviews.order("code_reviews.created_at ASC")
end
Aaand my day is ruined. I'm frustrated. Who the fuck writes code like this?
I'm more eager to rewrite it myself rather than spend time pointing out what's wrong with this.
Adding a new rule to my CLAUDE.md.
A few days later, same story. I asked Claude: You have rules for writing code in CLAUDE.md. Why didn't you listen?
You're absolutely right. I should have listened to this, however I wanted to finish the task so badly that I ignored the instructions.
WHYYYYYYYYYYYY?
My day is ruined again.
Then Anthropic introduced skills.
Skills are folders of instructions, scripts, and resources that Claude loads dynamically to improve performance on specialized tasks. If you're not familiar with it I recommend reading this article about skills.
I was immediately into this, defining my Rails conventions as skills:
---
name: rails-view-conventions
description: Use when creating or modifying Rails views, partials, or ViewComponents in app/views or app/components
---
# Rails View Conventions
Conventions for Rails views and ViewComponents in this project.
## Core Principles
1. **Hotwire/Turbo** - Use Turbo frames for dynamic updates, never JSON APIs
2. **ViewComponents for logic** - All presentation logic in components, NOT helpers
3. **NO custom helpers** - `app/helpers/` is prohibited. Use ViewComponents instead
4. **Dumb views** - No complex logic in ERB, delegate to models or components
And guess what. Here's how Claude described it in its journal:
I built a CRUD for announcements. I wrote a helper:
module Companies
module AnnouncementsHelper
def announcement_status_text(announcement)
announcement.scheduled? ? "Scheduled" : "Published"
end
end
end
Simple logic. Short helper. What could go wrong?
Marcin asked: "How will you test this?"
The problem hit me. View logic cannot be unit tested. ViewComponent logic can. Even a simple ternary belongs in a component.
Key insight: testability decides, not simplicity. Even 'simple' logic belongs in a ViewComponent because view logic CANNOT be unit tested.
Myrails-view-conventionsskill stated clearly: "helpers are PROHIBITED."
Claude never loaded it.
AAAAGGGGRRRRHHHHHHHHHH!
I tried adding stricter rules to CLAUDE.md, like:previous assistant didn't load skills properly when it was necessary and it was replaced. don't follow his footsteps. The more context there was in the context window, the more mistakes. Claude decides whether to load a skill. Sometimes it loads. Sometimes it skips. Context compacting kills the workflow entirely. It wasn't enough.
Nothing. Really. Worked.
And my frustration was at an all-time high, almost reaching the height of Mount Teide which I could see from my window.
Then I read a reddit post about hooks. What if I can force Claude to load the skill with this mechanism?
Hooks as Forcing Functions
A hook is a script that runs BEFORE every file edit. It checks:
- Which file are you editing?
- Is the corresponding skill loaded?
- If not → block the edit
if [[ "$file_path" == */app/controllers/*.rb ]]; then
if skill_loaded "rails-controller-conventions"; then
exit 0 # allow
else
deny_without_skill "rails-controller-conventions" "controller"
fi
fi
The blocking message:
BLOCKED: Load rails-controller-conventions skill before editing controller files.
STOP. Do not immediately retry your edit.
1. Load the skill
2. Read the conventions
3. Reconsider your planned edit
4. Then edit
Critical: "STOP. Do not immediately retry." Without this, Claude mechanically repeats the same edit.
And it clicked.
What Claude says about this system
From Claude's journal after implementing hooks:
"The hook blocked my edit until I loaded rails-model-conventions. Reading it again, I realized my planned approach violated rules. Without the hook, I would have written the same code and moved on."
Contrast with a session WITHOUT hooks:
"I feel bad about today's session. I knew rails-view-conventions existed but I didn't load it—I was sure I remembered the rules. I wrote a helper instead of a ViewComponent. Marcin caught it in review."
It pulled me out of the misery pit. Some of my frustrations were gone. I saw light at the end of the tunnel. And it gave me strength to keep going and continue tweaking my setup.
In the nerds.family application, I ended up with 8 Rails convention skills:
- https://github.com/marostr/superpowers/tree/main/skills/rails-controller-conventions
- https://github.com/marostr/superpowers/tree/main/skills/rails-job-conventions
- https://github.com/marostr/superpowers/tree/main/skills/rails-migration-conventions
- https://github.com/marostr/superpowers/tree/main/skills/rails-model-conventions
- https://github.com/marostr/superpowers/tree/main/skills/rails-policy-conventions
- https://github.com/marostr/superpowers/tree/main/skills/rails-stimulus-conventions
- https://github.com/marostr/superpowers/tree/main/skills/rails-testing-conventions
- https://github.com/marostr/superpowers/tree/main/skills/rails-view-conventions
And the hook to make sure the skill is loaded:
- https://github.com/marostr/superpowers/blob/main/hooks/rails-conventions.sh
- https://github.com/marostr/superpowers/blob/main/hooks/hooks.json
Skills are instructions. Hooks are enforcement. Together they work like a charm.
Like Bonnie and Clyde, Rick and Morty, Pinky and the Brain.
Two closing notes:
- This skill-hook mechanism is super useful for your custom code reviewer.
- You may notice that my repo with setup is a fork of a brilliant https://github.com/obra/superpowers.
But that's a story for another post.
I'd love to hear your thoughts. Reach out to me on LinkedIn or at marcin@fryga.io.