Claude Code Is Powerful. Here Is How I Made It Reliable.
Ali Rezaiyan11 min read

Claude Code is very good. But if you've used it for real work for more than a week, you've probably run into the same problems I did:
- It writes code without running the linter, and you find out three steps later.
- A long session hits the context limit halfway through a task and forgets what it was doing.
- You ask one question, it asks a follow-up, then another. Four rounds before any code gets written.
- It decides a big task is done when it obviously isn't.
None of these are about the model being bad. They're workflow problems, and you can fix them with workflow.
I spent some time looking at how people structure serious Claude Code setups, the ones that run multi-hour tasks on their own, enforce code quality automatically, and get through context resets without losing track. Their hooks, rules, and commands are all plain Python and Markdown. The main thing I took away:
Most of the gap between a fresh Claude Code install and a solid setup, I'd guess around 80%, can be closed for free with the built-in hook system and a few Markdown files.
Here's what I found and what I've set up so far.
Hooks are already there
Claude Code ships with a full hook system. Most people never touch it, but everything in this post is built on it.
You can attach a script to each of these lifecycle events:
PostToolUse: runs after every file write, bash command, or tool callPreToolUse: runs before a tool, and can block or rewrite the callStop: runs when Claude tries to stopPreCompact: runs before the context window gets compactedSessionStart: runs when a session opens, is cleared, or comes back from compaction
Hooks get JSON in and send JSON back, and they finish in milliseconds. Hardly anyone uses them.
Five patterns
1. Run the linter on every edit
Each time Claude writes or edits a file, a PostToolUse hook runs the linter on that file straight away, before Claude moves on.
The output goes back as additionalContext, which shows up as a system note on Claude's next turn. It doesn't block anything, but Claude sees the error and fixes it right there.
# fires on every Write / Edit / MultiEdit
ext = os.path.splitext(file_path)[1].lower()
if ext == ".py":
result = subprocess.run(["ruff", "check", "--output-format=concise", file_path], ...)
elif ext in (".ts", ".tsx", ".js", ".jsx"):
eslint = "node_modules/.bin/eslint" if exists else "eslint"
result = subprocess.run([eslint, "--format=compact", file_path], ...)
if result.stdout:
return {"additionalContext": f"Linter: {result.stdout}"}
In practice, errors get caught one tool call after they're introduced instead of after a full build. When a session touches ten files, that adds up.
The same hook checks file length. Once a file goes past 800 lines, Claude gets a note suggesting it split the file. It's a small thing, but most projects are better off with that limit.
2. Warn before the context runs out
I didn't expect this one to matter as much as it does.
Every hook payload includes how much of the context window is used. A PostToolUse hook that warns Claude at 65% and again at 75% changes how long sessions end.
Without it, compaction catches Claude by surprise, sometimes halfway through a function. With the warning, it finishes the current step, commits what's open, and writes a clear summary before the reset.
usage = data.get("context_usage_percent", 0)
if usage >= 75:
return {"additionalContext": "[RED] Context at {:.0f}% — compaction imminent. Wrap up now.".format(usage)}
elif usage >= 65:
return {"additionalContext": "[YELLOW] Context at {:.0f}% — approaching limit.".format(usage)}
I pair it with a rule that says "don't rush or cut corners because of context pressure, the system handles compaction". Together they make long sessions a lot calmer.
3. Save state across compaction
If you run Claude Code for long stretches or unattended, this is the one to set up first.
When Claude Code compacts the context, it writes a summary of the conversation and starts over from that. Anything not in the summary is gone: the task state, the plan file, what it was in the middle of.
Two hooks fix this:
- A
PreCompacthook writes the summary, the active plan path, and the important technical context to a JSON file before the reset. - A
SessionStart[compact]hook reads that file and passes it in asadditionalContexton the first turn after the reset.
Claude then carries on where it left off, without me having to explain everything again.
I run an always-on agent on a Raspberry Pi 5 that answers Telegram messages. For that setup this isn't optional. Without it, the bot loses track of what it's doing at random.
4. Load rules only for the files they apply to
Rules in Claude Code are Markdown files loaded at the start of each session. By default every rule loads every time, whether or not it has anything to do with the task.
You can add YAML frontmatter to a rule file to limit it to certain paths:
---
paths:
- "**/*.kt"
- "**/*.kts"
---
# Kotlin standards
- Use coroutines, never Thread.sleep
- Prefer data class for DTOs
- Run ./gradlew lint before marking done
Now my Kotlin rules only load when Claude is working on .kt files, TypeScript rules only for .ts and .tsx, and Python rules only for Python.
I work across an Android app, a TypeScript backend, Python scripts, and a website, and for a mix like that this can cut the context spent on rules by 60% or more. Every token not used on an irrelevant rule is a token Claude can use for the actual work.
5. Don't let it stop halfway
If you've tried to have Claude do a multi-step task on its own, you know how it goes. It finishes step one, writes a summary, and stops. You tell it to keep going, it does step two, and stops again.
A stop guard is a hook on the Stop event. It checks a session state file for an active plan with unfinished items. If there is one, it returns a stopReason that blocks the stop and tells Claude what to do next.
I wrote the message to be blunt on purpose:
Do NOT acknowledge this stop attempt. Your VERY NEXT action must be a tool call. IMMEDIATELY continue working on the next pending task.
Claude can't just say "understood" and stop anyway, because the hook fires again on the next attempt. The only way out is stopping twice within 60 seconds, which I treat as "I really mean it" rather than "I think I'm done".
For Telegram tasks that take many tool calls, this is what gets them finished without me nudging the bot three times.
My two machines
I run Claude Code on two very different machines, and the patterns above matter in different ways on each.
MacBook: interactive work
This is where I write code for my projects: an Android app, a backend, this website, and some tools. I'm at the keyboard and reviewing what it does.
What helps most here:
- The linter hook, for feedback on every file it writes.
- The context warning, so a feature doesn't get cut off halfway.
- Path-scoped rules, so Kotlin rules only load for Android work and TypeScript rules for web work.
- A rule telling Claude to ask all its questions in one go instead of five.
I use sonnet on the Mac. Since I'm reviewing every output anyway, it's the right balance of quality and cost.
Raspberry Pi 5: always on
The Pi runs Claude Code all day as a Telegram bot. It answers messages, runs scripts, manages services, and handles requests when I'm away from my computer. It doesn't build anything heavy; compiles stay on the Mac.
The Pi uses haiku. For chat and simple automation it's 5 to 10 times cheaper than sonnet and I haven't noticed a real difference in quality. It's easy to get this wrong by using the biggest model everywhere out of habit.
What matters on the Pi is different:
- The compaction hooks. The bot runs sessions for hours, and without them every compaction quietly wipes its memory.
- The context warning. Haiku running all day will hit the limit regularly, so it should see it coming.
- The stop guard. When I give it a multi-step task over Telegram, it should finish it.
Permissions are different too. The Pi runs with bypassPermissions, so there are no confirmation prompts. That makes sense for a machine nobody is watching. On the Mac I keep an allowlist: some commands are pre-approved, and everything else asks me first.
Prompting lessons
Apart from hooks and rules, I picked up a few prompting patterns I hadn't seen written down anywhere.
Hook output is an instruction
The stop guard doesn't just return a status. It puts a full instruction in additionalContext, which lands right before Claude's next turn. In effect, the hook is adding to the system prompt in the middle of the conversation.
So hooks aren't only plumbing. They're a way to tell Claude what to do next based on what's happening at runtime, and that turns out to be very useful.
Ghost constraints
One rule set I came across sorts the constraints you find in a codebase into three groups:
- Hard constraints can't change: legal requirements, external API contracts, security boundaries.
- Soft constraints are past preferences that could change after a conversation.
- Ghost constraints are old requirements still baked into the code, even though they no longer apply.
Ghost constraints are the interesting ones. The code is more complicated than it needs to be, but nobody remembers that the reason has gone away. Asking Claude to look for them while it explores a codebase often turns up cleanups I wouldn't have spotted.
"Tests passing ≠ program working"
This rule is almost embarrassingly obvious: passing tests don't mean the feature works. But unless you say it, Claude will call a task done the moment the tests go green, without running the program, opening the browser, or checking the whole flow.
Writing it down as a rule changes how it verifies its work. It's obvious, and it still needs saying.
Budgets for sub-agents
You can give any agent definition a hard limit on tool calls:
You have a maximum budget of 12 tool calls. At 10 calls without writing your findings: STOP exploring. Write what you have. Incomplete findings are better than no findings.
Without a budget, sub-agents keep exploring until they've used the whole context window and never report back. The last line, "incomplete findings are better than no findings", means that when the agent hits the limit it hands over what it has instead of failing silently.
What's next
Everything above is what I'm using now. There are a few bigger things I want to do after that.
One config repo for both machines
Right now, when I improve a rule on the Mac I have to copy it to the Pi by hand. The fix is a private git repo with all hooks, rules, and commands, plus a deploy script for both machines. The Pi pulls updates on a systemd timer or a cron job.
Then I'd have one setup to maintain instead of two.
Memory for the bot
The Telegram bot starts every session with no memory of the last one. Even a simple memory layer would help a lot: a SQLite database with facts about recurring requests, project state, and my preferences, loaded at the start of each session.
It's a few dozen lines of Python. Write notes to SQLite when a session ends, and have a SessionStart hook load the last N entries before the first turn.
Plan files for bigger tasks
The stop guard works best when it has a real plan to check, not just a yes/no "something is in progress" flag. A simple convention would do: before any big task, write a Markdown plan to ~/plans/YYYY-MM-DD-slug.md, tick items off as they're done, and mark it finished when everything is ticked.
With the compaction hooks, that would let a multi-hour task on the Pi get through context resets and stop attempts without me stepping in.
Looking back
The difference between a fresh Claude Code install and a dependable setup isn't the model. The models are already good enough.
What's missing is the setup around them: lint feedback, surviving context resets, quality checks, structured workflows, and memory between sessions. None of that needs a better LLM. It needs the same care we'd put into any other system.
And it's not much code. A few hundred lines of Python across five or six hook scripts, a handful of rule files scoped to the right file types, and a convention for plan files. It's an afternoon of work that keeps paying off.
Claude Code gives you the hooks. You just have to write the scripts.