Claude Code Hooks: A Practical Guide to Not Staring at Your Terminal
Good design is invisible. You only notice it when using something worse.
Claude Code Hooks: A Practical Guide to Not Staring at Your Terminal
Good design is invisible. You only notice it when using something worse.
**Cursor** might be expensive, but that notification icon on the menu bar was a beacon my eyes would track all day, waiting for it to tell me some work I had queued needed my attention, while I spent that time binging the downfall of humanity, and Heikin Ashi candles on Youtube (mutually exclusive to each other) or just doomscrolling on Instagram.
Until, I moved to Claude. The transition was almost smooth, results almost similar since I’ve always used Opus since it came out. I was definitely surprised about how Claude has hourly quotas, which took getting used to.
Objectively, after having used Claude Code for a good while now, I can say it is so much more tunable if you are opinionated as a developer. Indexing my project beforehand to speed up execution is nice, but grep was made to save humanity (you learn to love it the hard way sifting through 50MB log files) and I’m glad Claude prefers it. Engineering lives in log outputs, not code comments.
While that may be great, I still miss my notification icon. Turns out, a notification icon is a much bigger challenge that involves building native apps that can live in your Menu Bar, something Cursor could do as a standalone app, but we cannot. The next best thing? Something akin to the pop you get when your order executes on Zerodha’s Kite and your heart skips a beat. Well… something less anxiety-inducing perhaps. Enter Claude Hooks.
What Are Claude Code Hooks?
Claude Code has a hooks system that lets you run shell commands in response to specific events. This is familiar territory. Exactly like React hooks, but instead for the lifecycle of Claude’s execution loop.
The hooks that we care about, specifically for notifications:
Stop— Claude finishes responding (end of turn)
PermissionRequest— Claude is about to ask for tool approval
PreToolUse— Claude is about to use a specific tool
PostToolUse — After a tool finishes executing
Notification— Claude sends a notification event
Each hook receives a JSON payload on stdin with context about what's happening. Your script reads it, does whatever it wants, and exits.
Hooks are configured in ~/.claude/settings.json:
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "node /path/to/your/script.js"
}
]
}
]
}
}
That’s it. When Claude stops responding, it runs your script.
Step 1: The “Task Completed” Hook
The simplest and most useful hook. When Claude finishes a task, play a sound so you know it’s done.
Create a file called on-stop.js:
#!/usr/bin/env node
const { execSync } = require("child_process");
let raw = "";
process.stdin.setEncoding("utf-8");
process.stdin.on("data", (chunk) => (raw += chunk));
process.stdin.on("end", () => {
let input = {};
try { input = JSON.parse(raw); } catch { process.exit(0); }
// Don't fire if another stop hook is already running
if (input.stop_hook_active) process.exit(0);
// Play the Hero sound on macOS
try {
execSync('afplay "/System/Library/Sounds/Hero.aiff"', { stdio: "ignore" });
} catch {}
// Show a macOS notification
try {
execSync(`osascript -e 'display notification "Claude has finished the task." with title "Claude Code"'`, { stdio: "ignore" });
} catch {}
process.exit(0);
});
Register it in ~/.claude/settings.json:
{
"hooks": {
"Stop": [
{
"hooks": [{ "type": "command", "command": "node /path/to/on-stop.js" }]
}
]
}
}
Restart Claude Code, give it a task, and walk away. There will be a chime when Claude finishes.
Step 2: The “Needs Permission” Hook
Claude frequently pauses to ask for permission before running commands, editing files, or using tools. If you’re not watching, you won’t see the permission dialog, and Claude just… waits.
The PermissionRequest hook fires right when this happens:
#!/usr/bin/env node
const { execSync } = require("child_process");
let raw = "";
process.stdin.setEncoding("utf-8");
process.stdin.on("data", (chunk) => (raw += chunk));
process.stdin.on("end", () => {
let input = {};
try { input = JSON.parse(raw); } catch { process.exit(0); }
const tool = input.tool_name || "a tool";
// Play Glass sound — distinct from the completion sound
try {
execSync('afplay "/System/Library/Sounds/Glass.aiff"', { stdio: "ignore" });
} catch {}
try {
execSync(`osascript -e 'display notification "Claude needs permission to use ${tool}." with title "Claude Code"'`, { stdio: "ignore" });
} catch {}
process.exit(0);
});
{
"hooks": {
"PermissionRequest": [
{
"hooks": [{ "type": "command", "command": "node /path/to/on-permission.js" }]
}
]
}
}
Same here as the Stop notification, but a different chime for when Claude needs permission for a specific tool use.
Step 3: The “Asking a Question” Hook
Sometimes Claude doesn’t need permission — it needs your input. It’ll ask a question via the AskUserQuestion tool: "Which database should I use?" or "Do you want me to include tests?"
This is where PreToolUse comes in. It fires before any tool execution, and you can filter it to specific tools using the matcher field:
#!/usr/bin/env node
const { execSync } = require("child_process");
let raw = "";
process.stdin.setEncoding("utf-8");
process.stdin.on("data", (chunk) => (raw += chunk));
process.stdin.on("end", () => {
// Play Funk sound — yet another distinct sound
try {
execSync('afplay "/System/Library/Sounds/Funk.aiff"', { stdio: "ignore" });
} catch {}
try {
execSync(`osascript -e 'display notification "Claude is asking you a question." with title "Claude Code"'`, { stdio: "ignore" });
} catch {}
process.exit(0);
});
{
"hooks": {
"PreToolUse": [
{
"matcher": "AskUserQuestion",
"hooks": [{ "type": "command", "command": "node /path/to/on-question.js" }]
}
]
}
}
The matcher field is the key here. Without it, PreToolUse would fire for every tool call — every file read, every grep, every edit. With "matcher": "AskUserQuestion", it only fires when Claude is specifically asking you something.
Something interesting but predictable about this hook is that since it’s a pretool use hook with a matcher, there is a latency between when this hook triggers a notification, and when the question finally appears for the user. Minimal, but noticeable.
The Gotcha: Avoiding Double-Fires
Nothing with vibe-coding is ever as simple as it should be so here is the nuance with this specific problem statement: You want to fire different chimes for when Claude needs to ask you for user input vs. when it needs you to provide the permission to use a certain tool.
However, when Claude uses AskUserQuestion, it triggers both the PreToolUse hook (because AskUserQuestion is a tool) and the PermissionRequest hook (because using a tool requires permission).
That means two sounds would play simultaneously. Now we don’t like that, but we also do not like slop. Well, at least below the very low standards we set for ourselves of course.
The fix: in the permission hook, skip AskUserQuestion:
// In on-permission.js, right after parsing input:
if (input.tool_name === "AskUserQuestion") process.exit(0);
Now AskUserQuestion is handled exclusively by the PreToolUse hook, and everything else goes through PermissionRequest. Clean separation.
Putting It All Together
Here’s the complete settings.json with all three hooks:
{
"hooks": {
"Stop": [
{
"hooks": [{ "type": "command", "command": "node ~/.claude/hooks/on-stop.js" }]
}
],
"PermissionRequest": [
{
"hooks": [{ "type": "command", "command": "node ~/.claude/hooks/on-permission.js" }]
}
],
"PreToolUse": [
{
"matcher": "AskUserQuestion",
"hooks": [{ "type": "command", "command": "node ~/.claude/hooks/on-question.js" }]
}
]
}
}
Three hooks, three scripts, three distinct sounds.
What Else Can You Build With Hooks?
The principle extends to any usecase that plays around Claude’s different states. Hooks give you a programmatic interface into Claude Code’s lifecycle. Therefore, some ideas that follow:
- Auto-commit: Use a
Stophook to auto-stage and commit after every completed task - Logging: Track which tools Claude uses most with a
PostToolUsehook that appends to a log file - Cost tracking: Parse the
Stophook payload to track token usage across sessions - Slack/Discord alerts: Replace
osascriptwith a webhook call to get notifications on your phone - Guardrails: Use
PreToolUseto block specific commands or tools — return a non-zero exit code to prevent execution - Auto-review: Run a linter or type-checker in a
PostToolUsehook after every file edit
Try Out the Plugin:
If you just want the notifier without building it yourself:
VSCode:
code --install-extension SingularityInc.claude-notifier
CLI / Terminal / Vim:
curl -fsSL https://raw.githubusercontent.com/ashmitb95/claude-notifier/main/install.sh | bash
The extension auto-configures everything. The CLI installer copies the hooks and updates your settings. You can now go back to binging your favorite TV show while Claude works for you.
Cheers!
Source code: github.com/ashmitb95/claude-notifier

메타데이터
- post_id
- 71873d2f3f06
- slug
- claude-code-hooks-a-practical-guide-to-not-staring-at-your-terminal-71873d2f3f06
- url
- https://medium.com/@ashmitbbiswas/claude-code-hooks-a-practical-guide-to-not-staring-at-your-terminal-71873d2f3f06
- canonical_url
- https://medium.com/@ashmitbbiswas/claude-code-hooks-a-practical-guide-to-not-staring-at-your-terminal-71873d2f3f06
- author_url
- https://medium.com/@ashmitbbiswas
- status
- ok
- fetched_at
- 2026-07-21 06:32:45