Your Claude Code Hooks Are Costing You Minutes a Day — Here's How I Measured It A developer has created a shell script wrapper to measure the latency of Claude Code hooks, which can silently add minutes of waiting time per session. The wrapper, hook-latency-wrap.sh, uses bash's EPOCHREALTIME variable to log execution times without altering hook behavior, addressing a common performance issue in AI-assisted development workflows. If Claude Code feels sluggish lately, the culprit probably isn't the model — it's the pile of shell scripts you wired into it months ago and never looked at again. Going from $0/month to a real income in six months came down to a lot of small habits, and one of them was refusing to leave my Claude Code hooks unmeasured. Claude Code has a feature called "hooks." It's a simple mechanism, wired up in settings.json , that lets you inject arbitrary shell scripts before and after tool calls. Right now I have a dozen-plus hooks bundled across three types — PreToolUse, PostToolUse, and Stop — running everything from automatic git commits to latency reports, self-audits, and project categorization. The problem is that hooks run dozens of times in a single session . Say Claude Code edits files 10 times in one session. If a PostToolUse hook fires each time, the execution time of that one hook × 10 becomes pure waiting cost. A lot of people never notice this and just feel like "Claude Code got slower somehow." I was one of them — I once had a heavy Python-based process wired into my self-audit hook, and everything felt sluggish. It took me days to figure out why. Perceived "heaviness" is proportional not to the number of hooks, but to the latency of each one. Three hooks are fine if they all finish under 50ms. But a single hook with a p95 above 2000ms racks up 20 seconds of pure waiting after just 10 calls. The operator is doing nothing and losing 20 seconds. Run several sessions a day, and you're burning minutes — or tens of minutes — without realizing it. You can't spot this loss by staring at the hooks section of settings.json . All that's written there is a command string; nothing records how many milliseconds it takes. That's why measurement is the only answer . What matters here is that you can start with zero configuration changes . The hook-latency-wrap.sh script described below is just a shell script that takes an existing hook binary as an argument and wraps it. Without altering the original hook's behavior at all, it writes the elapsed time and exit code of each invocation to a log file in JSONL format. You don't need to rewrite your production hook logic to instrument it. Most people using Claude Code for side projects or solo development get absorbed in just getting hooks working and never get around to measuring them. I was the same. Every time I added a hook, I felt "upgraded" and left it at that. But that's close to loading a car with so many parts that fuel economy tanks while you celebrate the "mods." Precisely because Claude Code is an autonomous agent, the density of a single session matters. The time between issuing an instruction and getting a response shifts a lot with accumulated hooks. I think of it as "cutting hook latency = raising my own hourly rate." Behind a figure like ¥1.2M monthly revenue is a stack of unglamorous habits, including making the tools I use as fast as possible. $EPOCHREALTIME The wrapper script I'm showing here doesn't use Python — it measures with the bash builtin variable $EPOCHREALTIME . This variable is available in bash 5.0 and later and returns the current time in seconds.microseconds format e.g. 1720000000.123456 . Why not call Python? Because it defeats the purpose if the measurement script itself becomes heavy. Python3 startup can cost tens to hundreds of milliseconds depending on the environment. If you invoke Python every time just to measure, that startup cost contaminates the measurement itself. With a bash builtin, you get the current time at microsecond resolution without spawning an additional process. That said, if bash is older than 5.0 like the default shell on older macOS , $EPOCHREALTIME comes back empty. The implementation includes a Python3 fallback for that case. I'll cover it in the code walkthrough in the next section. Here's how the whole system is structured. settings.json └─ command: "hook-latency-wrap.sh 本来のhook.sh" │ ├─ 本来の hook.sh を実行(動作は変わらない) │ └─ 経過時間・終了コード を JSONL に追記 │ ~/.claude/logs/hook-latency.jsonl │ hook-latency-report.sh days │ ターミナルに集計表を出力 (hook名・回数・mean・p95・max・fail) The wrapper writes to JSONL, and the reporter reads the JSONL and aggregates it. Two scripts, one log file. The only change to settings.json is "prepend the wrapper's path to the command string." It's 43 lines total. Here's the actual code, verbatim. bash /usr/bin/env bash hook-latency-wrap.sh — 任意の hook をラップして実行時間を JSONL に記録 使い方: settings.json で command を以下に置き換える: "command": "~/.claude/scripts/hook-latency-wrap.sh /path/to/hook.sh" 出力: ~/.claude/logs/hook-latency.jsonl 1行 = 1呼び出し {"ts":"...","hook":"...","elapsed ms":N,"exit code":N} set -uo pipefail HOOK BIN="${1:-}" -z "$HOOK BIN" && { echo "usage: $0