A Second Note on Claude Code
Harnessing Hooks, Rules and Plugins to full effect
A Second Note on Claude Code
Harnessing Hooks, Rules and Plugins to full effect

A joint article on Claude Code. Image generated by ChatGPT
Authors’ Note: This article is jointly written by James Koh, PhD and Tituslhy (“Titus”). Since Titus’ previous Medium article on Claude Code, Titus and James have been comparing notes from their individual use of Claude Code. There were simply too many lessons , so here we are. We’ve done our best to blend our writing styles: James writes the way he teaches, and Titus writes more tongue-in-cheek (his MITB groupmates will know exactly what that means).
As in Titus’ MITB days, James has the final say. (Removed by James, and then reinstated subsequently)
We want to start with a confession.
There is no specific GitHub repository for this article. Instead, what follows is a collection of lessons harvested from multiple projects we scaffolded purely to learn Claude Code.
Titus’ previous article covered: (i) designing CLAUDE.md files, (ii) sub-agent architectures, (iii) custom commands, and (iv) the Ralph Wiggum loop — a technique for terraforming an entire code repository into existence after uv init.
This article extends some of those lessons, quietly buries others, and introduces a few you haven’t seen before. Section 0 is only for those who have never used Claude CLI before. If you are already using the CLI, go straight to Section 1.
0. Starting set-up
To start off, I first want to go back to the very basics, of how to get Claude Code on your CLI to begin with. Creating a guide with screenshots isn’t easy when everything is already up and running… Good thing I have another (Windows) laptop and can start everything from scratch.
First, open Powershell and type the following. Note that the command is different if you use CMD.
irm https://claude.ai/install.ps1 | iex
You will see the nice-looking ‘successfully installed!’ text in green. However, if you rush on attempting to launch claudenow, there will be an error. You need to set up the PATH, which can be done by pasting the command line below in your Powershell. There is no hardcoding, and hence you can use it directly as is without worrying that our usernames differ.

[Environment]::SetEnvironmentVariable("PATH", "$env:USERPROFILE\.local\bin;$([Environment]::GetEnvironmentVariable('PATH', 'User'))", "User")
(back to my main laptop now..)
Launch ClaudeCode on the terminal. Note that you must see the claude picture to know that you are in. Perform /login (Claude suggests auto-complete for you).

If you authenticate via the first option, you will be given a link to paste into the browser.


You should see a green Login Successful.

Suppose you exit (with Ctrl+C) and re-enter (with claude), you will see a welcome back message, without needing to log in again.

Now, install the Langsmith plugin in Claude Code. Note that your Git ssh keys should already be set up when running the following, because the process involves cloning from https://github.com/langchain-ai/langsmith-claude-code-plugins.git.
/plugin marketplace add langchain-ai/langsmith-claude-code-plugins
/plugin install langsmith-tracing@langsmith-claude-code-plugins
/reload-plugins
This will be helpful for section 4 later.
1. Extending CLAUDE.MD files in Claude Code
In his previous Medium article, Titus made the case for keeping CLAUDE.md lean and focused. That advice holds, but as projects get larger, we may need to include architectural guidelines, domain knowledge, testing requirements, or other specialized guidelines. If everything is inside a single markdown file, we may end up with a wall-of-text which no one wants to touch, as well as take up a chunk of the context window even though not everything is required.
Claude Code addresses this problem through ‘rules’. That is, individual Markdown files stored under .claude/rules/. Instead of one large instruction file, you split project knowledge into smaller focused files like database.md, error-handling.md, coding-standards.md, testing.md. Every file in the directory is discovered recursively, so you can organize them into subfolders just like how you actually think about the project. For example the following.
yourprojectname/
└── .claude/
├── CLAUDE.md
└── rules/
├── architecture/
│ ├── database.md
│ └── integrations.md
├── development/
│ ├── error-handling.md
│ └── coding-standards.md
├── frontend/
│ ├── react.md
│ └── styles.md
└── quality/
├── acceptance-criteria.md
└── testing.md
An obvious benefit is maintainability. We can keep the data engineering stuff separate from the frontend stuff. The more interesting thing is on how each rule is loaded.
Always-on rules
A rule with no frontmatter is loaded at launch, with the same priority as .claude/CLAUDE.md. For example, within .claude/rules/architecture/database.md (see above structure) we can have:
# Database Rule
Always use parameterized queries. Never interpolate user input directly
into SQL strings.
Do not expose raw database errors to users.
We should be clear about the benefit. An always-on rule enters the context window every single session, just like CLAUDE.mddoes. Splitting your instructions across numerous always-on rule files instead of one CLAUDE.mdsaves nothing in tokens. We still benefit from neater organization, but there’s no difference in terms of context.
Path-scoped rules
A rule becomes path-scoped by adding a paths: field to the rule’s frontmatter. Claude loads that rule only after it first reads a file whose path matches one of the specified glob patterns. If your session never touches those files, the rule never enters the context window. This is where context savings come in.
For example, .claude/rules/architecture/database.md could contain:
---
paths:
- "data/**"
- "src/services/**"
---
# Database Rules
Always use parameterized queries. Never interpolate user input directly
into SQL strings.
Do not expose raw database errors to users.
The folders under .claude/rules/ do not need to mirror the project directories exactly. Their purpose is to organise the instructions. The paths: field inside each rule determines which source files the rule applies to.
The full project directory could be something like
yourprojectname/
└── .claude/
│ ├── CLAUDE.md
│ └── rules/
│ ├── architecture/
│ │ ├── database.md
│ │ └── integrations.md
│ ├── development/
│ │ ├── error-handling.md
│ │ └── coding-standards.md
│ ├── frontend/
│ │ ├── react.md
│ │ └── styles.md
│ └── quality/
│ ├── acceptance-criteria.md
│ └── testing.md
├── src/
│ │ ├── services/
│ │ ├── models/
│ │ └── util/
├── frontend/
├── data/
└── scripts/
In this example, the database rule above (in .claude/rules/architecture/database.md) applies only to files under data/ and src/services/.
If you start a new session to work exclusively on frontend/, the database rule never enters the context window. That’s the real token saving right there.
Things to note about rules
Path-scoped rules are triggered on read, not on write. If Claude creates a new file matching the rule’s path without first reading another matching file, the rule does not kick in. For important conventions that must be applied whenever Claude creates a new file, keep the rule always on or enforce it with a hook.
A word of caution that you should not treat rules like things Claude ‘remembers’, because that is not the case. Rules are context that gets discovered and loaded (or missed if not set up properly). We want to put domain-specific guidance into path-scoped rules so it appears only when relevant, and use always-on rules if you want it to hold everywhere, keeping CLAUDE.mdas a short list of facts.
Rules are delivered as context after the system prompt, which is why they act as guidance but not guarantees. If you need something to always happen at a specific moment , such as blocking potentially destructive command, the a PreToolUse or PostToolUse hook is the right way to go. Rules shape behavior, while hooks enforce it (which brings us to the next section).
2. Run custom logic with Hooks
What are Hooks?
Hooks are user-defined actions (shell commands, scripts, LLM prompts) that fire automatically at specified points in Claude Code’s lifecycle. The ‘points’ could be before Claude runs a shell command, or after it edits a file, or even when Claude thinks it’s done. We don’t need to simply hope that Claude behaves as instructed, we want it to 100% happen.
Once defined, Claude Code monitors the event listeners in your hook scripts, mapping an event (like PreToolUse) and an optional matcher (like Bash) to a handler. When a matching event occurs, Claude passes JSON context to the hook handler, which can inspect the input, take action, and return a decision.
Hooks are configured in .claude/settings.json, and can be inspected using the /hooks command inside Claude Code as well. You may also use .claude/settings.local.jsonfor project-specific settings that should apply only to you. It takes precedence, and you should add to .gitignore.
Example use-case for hooks
An example that provides practical value-add even for personal projects is to log all changes made with a time-stamp, and a one-sentence summary. First, we create the following in .claude/settings.json.
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write|NotebookEdit",
"hooks": [
{ "type": "command", "command": "python \"${CLAUDE_PROJECT_DIR}/.claude/hooks/log_edit_summary.py\"", "timeout": 30 }
]
}
]
}
}
Notice the nested key PostToolUsewithin the json. It is a type of event.
What are Events?
Events are specific ‘points’ (or moments) that happen in the lifecycle of a session. Hooks can listen for these.
In Claude’s official documentations, ~30 different events are defined, along with a description of when it fires (ie. gets triggered). The screenshots below shows some example to illustate.

Screenshot from https://code.claude.com/docs/en/hooks-guide#how-hooks-work, on first set of events.

Screenshot from https://code.claude.com/docs/en/hooks-guide#how-hooks-work, on second set of events. There are still more, until the ‘SessionEnd’ event.
In our example settings.json, we included aPostToolUse which fires after a tool call finishes.
What then are the tools? Basically anything below. Claude also explicitly stated in its documentations that PostToolUse “matches on tool name, same values as PreToolUse”.

Screenshot from Claude https://code.claude.com/docs/en/hooks#pretooluse
Note that things like Edit and Write are not commands. Instead, they are built-in Claude Code tools.
Hook Execution
Back to our same example, we have "matcher": "Edit|Write|NotebookEdit". Since it is under PostToolUse, what happens will be that after Claude calls Edit or Write or NotebookEdit, it executes the command python log_edit_summary.py (where I truncated the file path for readability).
The python script (disclaimer: I generated it with Claude), would look something like the following:
#!/usr/bin/env python3
"""PostToolUse hook: summarize an Edit/Write/NotebookEdit in one sentence via `claude -p`
and append it to logs/edit-log.md. Must never crash or block the user's session."""
import json
import os
import subprocess
import sys
import tempfile
from datetime import datetime, timezone
MAX_SNIPPET = 2000
MAX_SUMMARY = 200
def truncate(text, limit):
if text is None:
return ""
text = str(text)
return text if len(text) <= limit else text[:limit] + "...(truncated)"
def build_blob(tool_name, tool_input):
if tool_name == "Edit":
old = truncate(tool_input.get("old_string"), MAX_SNIPPET)
new = truncate(tool_input.get("new_string"), MAX_SNIPPET)
return f"--- before ---\n{old}\n--- after ---\n{new}"
if tool_name == "Write":
content = truncate(tool_input.get("content"), MAX_SNIPPET)
return f"--- new file content ---\n{content}"
if tool_name == "NotebookEdit":
source = truncate(
tool_input.get("new_source") or tool_input.get("content"), MAX_SNIPPET
)
cell_id = tool_input.get("cell_id", "unknown")
edit_mode = tool_input.get("edit_mode", "unknown")
return f"--- notebook cell {cell_id} ({edit_mode}) ---\n{source}"
return truncate(json.dumps(tool_input), MAX_SNIPPET)
def get_summary(file_path, blob):
prompt = (
"In one short sentence, describe what this code edit changed. "
"Respond with only the sentence, no preamble.\n\n"
f"File: {file_path}\n\n{blob}"
)
try:
result = subprocess.run(
["claude", "-p", "--disallowedTools", "Edit,Write,NotebookEdit,Bash"],
input=prompt,
capture_output=True,
text=True,
timeout=25,
cwd=tempfile.gettempdir(),
)
first_line = result.stdout.strip().splitlines()[0] if result.stdout.strip() else ""
return truncate(first_line, MAX_SUMMARY) or "(summary unavailable)"
except Exception:
return "(summary unavailable)"
def main():
try:
payload = json.load(sys.stdin)
except Exception:
return 0
tool_name = payload.get("tool_name", "unknown")
tool_input = payload.get("tool_input", {}) or {}
file_path = (
tool_input.get("file_path")
or tool_input.get("notebook_path")
or "(unknown file)"
)
blob = build_blob(tool_name, tool_input)
summary = get_summary(file_path, blob)
project_dir = os.environ.get("CLAUDE_PROJECT_DIR", os.getcwd())
log_path = os.path.join(project_dir, "logs", "edit-log.md")
os.makedirs(os.path.dirname(log_path), exist_ok=True)
timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
line = f"- **{timestamp}** — `{file_path}` ({tool_name}): {summary}\n"
try:
with open(log_path, "a", encoding="utf-8") as f:
f.write(line)
except Exception:
pass
return 0
if __name__ == "__main__":
sys.exit(main() or 0)
It resides within .claude/hooks/ and, when invoked, executes claude -p --disallowedTools "Edit,Write,NotebookEdit,Bash" amongst other things. The list of tools explicitly disallowed is there to ensure we won’t end up in any recursive loop (ie. avoid the scenario where invoking the hook invokes yet another hook etc.). The ‘one-sentence summary’ is obtained via a prompt, within the get_summary function.
The end result is that when Claude makes changes to your repo, you get a time-stamped line added to your logs, something like the following:

Having covered rules and hooks, let us move on.
3. Working entirely offline?
All Titus’ nephew wanted was to add the No Batidao song to a piano app. There was one problem. Titus was showing off the app on an iPad. No laptop. No Claude Code CLI.
Context: Titus was testing out Cursor at the time, and promptly burned through his entire monthly free-tier token limit in three hours. The casualty of that experiment: a piano web app for his children, vibe-coded into existence. The logic: expose them to music without buying an actual instrument, which would cost significantly more than a Claude/Cursor Pro subscription and take up space in a Singaporean apartment that has no room for a piano.
Titus’ children loved it. The nephew was less impressed. He had no interest in “Twinkle Twinkle Little Star”; he had more acquired taste. Specifically, energetic Brazilian funk music.
In the previous article, Titus covered running Claude Code in remote control mode, but that requires the CLI running on an actual machine. It turns out, Claude Code also has cloud sandboxing. Navigate to the Claude app, select “Code”, create a new session, and point it at your GitHub repository:

Screenshot of Titus’ Claude Code Mobile app
Claude spins up a cloud container, clones the repo, and you’re off.

Screenshot of Titus’ Mobile of Claude Code
From there, you simply chat with Claude to ship code. When it’s done, it seeks your approval before committing to a new feature branch. Features, shipped from a phone, while babysitting a nephew with niche music taste.
One important caveat: this only works if your latest code is already pushed to GitHub. If it’s sitting uncommitted on your local machine, Claude can’t see it — it clones from source.

Claude created and published a new feature branch with its code while Titus was entertaining his nephew.
4. Add Observability to Claude Code
Data scientists decide on data, not vibes.
Adding observability confers insight into Claude Code’s trajectory (how it got to its final answer step by step), and the individual inputs and outputs of each step it took.
Adding observability gives you insight into Claude Code’s full trajectory — how it reached its answer, what steps it took, and what each step cost. Without it, you’re essentially reviewing the final painting without ever seeing the brushstrokes.
Practical applications:
- Comparing tools: Give Claude Code and Codex the same task, trace both, and compare token spend, reasoning quality, and raw output — before committing to an annual subscription.
- Choosing your model: Should you run Opus or Sonnet? That’s a data question. Analyze a corpus of traces before deciding.
Though Titus uses LangSmith here, most major LLM observability platforms work with Claude Code.
Setup: Sign up for LangSmith, grab an API key, create .claude/settings.local.json, and add:
{
"env": {
"TRACE_TO_LANGSMITH": "true",
"CC_LANGSMITH_API_KEY": "<LangSmith API key>",
"CC_LANGSMITH_PROJECT": "my-project"
}
}
That’s the entire setup. LangSmith will automatically pick up Claude Code traces.

Screenshot of Titus’ LangSmith page
For context on value: Titus deliberately burned through his full session token limit on the piano project. LangSmith reported this would have cost USD $10 on a raw API key. The Claude Pro subscription at USD $20/month is great value for money.
An advantage of LangSmith is its copilot function. Chat with the platform, offload trace analysis to an LLM, and get actionable recommendations without having to read hundreds of spans yourself.

Screenshots from Titus’ LangSmith. Note: An LLM API key is needed. PS: Titus runs the “/compact” command frequently in Claude Code.
5. Tap into Claude Code’s Rich Plugin Ecosystem
One underrated advantage of Claude Code’s CLI over other AI coding tools is its plugin marketplace — and it moves fast. New plugins land here first, before trickling out to other copilots like Codex.
To explore, type:
/plugins
You’ll see the full marketplace, sorted by installs. Here are some of our favorites.
a. Superpowers
Titus’ previous article covered setting up multi-agent systems manually — planning requirements, defining roles, spawning subagents. The “Superpowers” plugin teaches Claude’s main agent to do all of that on its own, without being explicitly asked.
Titus wanted to add a “play along” feature to his piano app but forgot to configure subagents. Claude did it anyway: delivered a comprehensive task list, spawned subagents, and even assigned a Claude Opus subagent specifically for the final code review. Titus was initially worried about the token cost of Opus doing sub agent work. He needn’t have been — Sonnet handled orchestration, Opus only stepped in for the review pass.

Titus’ Claude Code agent suddenly used its superpower skill to develop a feature

Tempted to not write your own sub agents?
Defining subagents forces you to decompose the problem, which means Claude inherits your decomposition instead of having to derive it. That’s always cheaper than letting the main agent figure out its own task structure mid-flight. Superpowers is doing that decomposition work for you, which is convenient but costs tokens to reason through it.
For complex projects where you actually understand the architecture, your decomposition will almost certainly be tighter than what Superpowers derives autonomously.
For Ralph-loop style greenfield scaffolding, where you’re running for a long time and the task structure is knowable upfront, pre-defined subagents are the right call. Each subagent handles its slice, returns a tight summary, and the main agent’s context stays relatively clean because it never had to do the work itself. The key is that the summaries are bounded. A subagent that builds the database layer returns “done, here’s what I built” — not the entire build log.
The two approaches (defining custom sub agents or leaving it to superpowers) are not competing — they’re situational. Define your own subagents when you understand the architecture upfront. Let Superpowers handle decomposition when you’re exploring, debugging, or the task structure isn’t yet clear. The plugin earns its keep precisely in the moments where you haven’t thought it through yet.
b. Playwright
Before signalling completion, Claude ran a battery of automated tests — including launching Titus’ piano app in Chrome, interacting with it like a real user, and taking screenshots for analysis. All this made possible by the Playwright plugin.
Titus was looking at his phone when his laptop started playing the piano by itself.

Example of a Playwright screenshot made by Claude without any interference
Claude had found a bug in its own code, fixed it, and shipped.
c. Frontend Design
Titus builds almost exclusively in Chainlit and Streamlit — not exactly known as the glamorous end of the UI spectrum. And yet his vibe-coded apps look genuinely better than his coded apps. The Frontend Design plugin is a big reason why: tell Claude what you want to build, and it applies design best practices automatically — typography, spacing, color, layout — without you having to ask. For developers who know what good UI looks like but couldn’t describe why, this plugin quietly does the heavy lifting.
d. Context7
Context7 gives Claude the ability to pull live, version-specific documentation and code examples from source repositories directly into its context window. The practical effect: fewer hallucinated APIs, fewer deprecated method calls, fewer “why is this not working” moments that turn out to be a version mismatch from 2022.
One honest caveat: the quality of this plugin is only as good as the docs it pulls from. Well-maintained libraries shine here; poorly documented ones less so. Browsing the codebase directly is often more reliable — but it burns tokens fast (PS we both recommend looking through the code base yourself). Context7 is a reasonable middle ground, and worth having in your toolkit.
e. Ralph Loop
Covered in part (d) of Titus’ previous article. If you’re starting a greenfield project and want to go from empty repo to production-ready scaffold without writing boilerplate by hand, this is the one to reach for first.
—
There are far too many excellent Claude Code plugins to cover here, and we barely scratched the surface. Suggestion: before starting any new project, browse Claude’s official plugin page. You’ll find integrations you didn’t know you needed — for example, Claude Code can even invoke Postman directly, which means API testing without leaving your terminal.
Conclusion
When we started experimenting with Claude Code, we thought we were learning a new coding tool.
We were wrong. We were really learning a different way to build software.
Good prompts matter, good engineering matters more
Good engineering means decomposing problems, creating reusable rules, instrumenting your workflows, choosing the right tools, and building feedback loops that make every future project easier than the last. Claude Code just happens to be an excellent environment for putting those ideas into practice.
Whether you’re building an enterprise platform, a weekend side project, or games for children (as Titus frequently does), the lesson is the same:
Don’t think of AI as something that writes code for you. Think of AI as a team you’re leading. Like every good engineering team, the better the architecture, tooling, documentation, and processes, the better the outcomes become.
We’re still in the early days. The plugin ecosystem is growing weekly. Models are improving monthly. Entire workflows that felt impossible a year ago now feel routine.
Perhaps the biggest shift isn’t technological at all. It’s that software engineering is quietly becoming less about typing code and more about designing systems that continuously amplify human capability.
Right now, the only real bottleneck isn’t what AI can build — it’s how ambitious we’re willing to be. And we’re incredibly excited to be a part of this future.
Disclaimer: All opinions and interpretations are that of the writer, and not of MITB. I declare that I have full rights to use the contents published here, and nothing is plagiarized. I declare that this article is written by me and not with any generative AI tool such as ChatGPT. I declare that no data privacy policy is breached, and that any data associated with the contents here are obtained legitimately to the best of my knowledge. I agree not to make any changes without first seeking the editors’ approval. Any violations may lead to this article being retracted from the publication.
메타데이터
- post_id
- c521245ffc35
- slug
- a-second-note-on-claude-code-c521245ffc35
- url
- https://medium.com/mitb-for-all/a-second-note-on-claude-code-c521245ffc35
- canonical_url
- https://medium.com/mitb-for-all/a-second-note-on-claude-code-c521245ffc35
- author_url
- https://medium.com/@byjameskoh
- status
- ok
- fetched_at
- 2026-07-08 23:38:59