I started freelancing in college at ¥100k a month, stacked side gigs up to ¥600k, then got laid off and dropped to zero overnight. Over the next six months I built an autonomous Claude Code setup from scratch, and it now runs at ¥1.2M a month in revenue. Of everything that went into that, the bug that quietly ate the most time was the one where I tried to make something faster and made it slower instead.
Why This Setup Works
“Working” and “the environment is running” are two different things
When you’re trying to grow revenue as a solo developer, the first wall you hit isn’t how fast you write code — it’s the lag in verification. If you catch a type error the instant you write it, fixing it takes a minute. If you catch it 30 seconds later, you have to reload the surrounding context from memory. At 60 seconds you’re already drifting into the next task, so the context switch costs even more.
Claude Code has a mechanism that calls a PostToolUse hook after every tool execution. Put tsc --noEmit there and a type check runs every time the agent edits a file; if there’s an error, feedback flows straight back into the next instruction. The structure closes the “write a file, then verify it” loop without a human in the middle.
The problem is that as the project grows, that “straight back” falls apart.
Why tsc’s cold start is so heavy
The TypeScript compiler loads the entire dependency graph on startup. With 100 files it’s snappy, but once imports chain out and the set of modules actually referenced balloons into the hundreds, re-parsing from zero every time is no small cost. When I measured it on a project called closet-os, a cold tsc --noEmit took 30–60 seconds. An environment where the hook blocks for 60 seconds on every tool call isn’t autonomous — it’s an obstacle that stops work.
The --incremental option exists to solve this. It does the full analysis only on the first run and caches the result in a .tsbuildinfo file. Subsequent runs re-analyze only the diff, so if the change is small it finishes in 1–3 seconds. For a hook, it looked like the ideal choice.
The case where it backfires
But when I added --incremental to the closet-os hook, cold runs got 3.6x slower.
The cause was the .tsbuildinfo file bloating. The bigger the project, the longer the initial cache generation for incremental diff data takes. On top of that, as the cache accumulates and the file size grows, the overhead of reading that giant JSON on every run stops being negligible. The “only process the diff” benefit was being outweighed by the “load a heavy cache every time” cost.
For small-to-medium projects, incremental wins overwhelmingly. But on large projects, plain --noEmit is actually faster — the relationship inverts. And since the hook reuses the same script across every project, I needed “a mechanism that switches automatically based on project size.”
The threshold guard idea
The cache file’s size correlates strongly with project size. Small .tsbuildinfo means incremental works; large means normal mode is faster. From that observation I arrived at a one-line guard: “if the cache file exceeds 200KB, fall back to normal mode.”
The threshold was set from measurements. I confirmed both that the closet-os cache was around 200KB at the point where it started getting slow, and that projects smaller than that were consistently faster with incremental. It’s not a magic number; it’s a boundary based on real data.
The Overall Flow
The full picture of the hook
Claude Code がファイルを編集 (PostToolUse)
│
▼
post_tsc_check.sh 起動
│
├─ tsconfig.json が無い → exit 0(即終了)
│
├─ timeout コマンド解決
│ ├─ gtimeout (GNU coreutils) があれば優先
│ └─ なければ timeout / フォールバック
│
├─ TSBUILDINFO のサイズ確認
│ ├─ ファイル無し or ≤200KB → --incremental モード
│ └─ >200KB → 通常モード(フォールバック)
│
├─ tsc 実行(timeout 60s)
│ ├─ --incremental: --noEmit --pretty false --incremental --tsBuildInfoFile
│ └─ 通常: --noEmit --pretty false
│
├─ rc=124 (timeout) → 警告メッセージ・exit 0
└─ エラーあり → 型エラー表示・exit 0(エージェントはログを受け取る)
Enter fullscreen mode Exit fullscreen mode
The Claude Code agent can use the hook’s output directly in its next reasoning step. When there are type errors they’re printed with the prefix “=== TypeScript型エラー検出 ===” (TypeScript type errors detected), so the agent reads the errors itself and enters a fix loop.
The real code: from size check to branching
Let’s read the core of ~/.claude/hooks/post_tsc_check.sh in order.
① Defining the cache path
CACHE_DIR="node_modules/.cache"
[ -d "node_modules" ] && mkdir -p "$CACHE_DIR" 2>/dev/null
TSBUILDINFO="$CACHE_DIR/tsc-hook.tsbuildinfo"
Enter fullscreen mode Exit fullscreen mode
The .tsbuildinfo is pinned at node_modules/.cache/tsc-hook.tsbuildinfo. The reason it’s not in the project root is that node_modules/ in .gitignore already covers it. The filename tsc-hook also keeps its namespace separate from the app’s own build cache.
② The 200KB guard
USE_INCREMENTAL=1
if [ -f "$TSBUILDINFO" ]; then
SIZE=$(stat -f %z "$TSBUILDINFO" 2>/dev/null || echo 0)
if [ "${SIZE:-0}" -gt 204800 ]; then
USE_INCREMENTAL=0
fi
fi
Enter fullscreen mode Exit fullscreen mode
stat -f %z is how you get a file’s byte count with macOS’s stat (the equivalent of Linux’s stat -c %s). 2>/dev/null || echo 0 provides a zero fallback on error, so it runs safely on the first invocation when the file doesn’t exist yet. 204800 is 200 × 1024, i.e. 200KB in bytes.
③ Building the arguments and running
if [ "$USE_INCREMENTAL" = 1 ]; then
TSC_ARGS="--noEmit --pretty false --incremental --tsBuildInfoFile $TSBUILDINFO"
else
TSC_ARGS="--noEmit --pretty false"
fi
if [ -n "$TIMEOUT_CMD" ]; then
result=$($TIMEOUT_CMD npx tsc $TSC_ARGS 2>&1 | head -30)
rc=$?
else
result=$(npx tsc $TSC_ARGS 2>&1 | head -30)
rc=$?
fi
Enter fullscreen mode Exit fullscreen mode
--pretty false turns off color codes. The hook’s output goes into the agent’s log, not a terminal, so ANSI escape sequences mixed in would hurt readability. head -30 prevents an output explosion when errors pour out in bulk.
④ macOS support for timeout
TIMEOUT_CMD=""
if command -v gtimeout >/dev/null 2>&1; then
TIMEOUT_CMD="gtimeout 60"
elif command -v timeout >/dev/null 2>&1; then
TIMEOUT_CMD="timeout 60"
fi
Enter fullscreen mode Exit fullscreen mode
macOS has no /usr/bin/timeout (on the BSD side, timeout comes as gtimeout from Homebrew’s coreutils). We check for existence with command -v before assigning, and if neither exists, TIMEOUT_CMD="" stays empty and it runs without a timeout. Written in this order, the same script works on Linux, macOS, and CI.
⑤ Detecting a timeout
if [ "$rc" = 124 ]; then
echo "=== TypeScript check timeout (60s exceeded — tsc 多重実行/巨大依存変更の疑い) ==="
exit 0
fi
Enter fullscreen mode Exit fullscreen mode
rc=124 is the exit code when timeout / gtimeout kills the process. It’s treated as a warning rather than an error, and returning exit 0 doesn’t block the agent’s flow. The message mentions “suspected concurrent execution” because it actually happened: when Claude Code runs tools in parallel, multiple tsc processes start at the same time and corrupt each other’s cache.
Why return exit 0
Even with type errors, the script ends with exit 0. With exit 1, the hook is treated as “failed,” and there are cases where Claude Code’s tool execution itself gets interrupted. Type errors are information for the agent, not a condition for halting the flow. If you write the error content to stdout, the agent reasons “there’s a type error, so fix it” in its next step. It’s a design that separates error detection from flow control.
Implementation Details (continued)
Why the tsconfig.json check is the first line
[ -f "tsconfig.json" ] || exit 0
Enter fullscreen mode Exit fullscreen mode
The script begins with this one line. PostToolUse hooks fire in every project, so they trigger in repositories that aren’t TypeScript at all. Putting the tsconfig.json existence check at the top means Python projects or Bash-only directories exit immediately with exit 0.
The key point is that this check runs in the direction of “exit if tsconfig.json is absent,” not “exit on failure.” When [ -f "tsconfig.json" ] is true, exit 0 doesn’t run (the right side of || is evaluated only when the left side is false). If you’re not used to reading this one-liner it’ll confuse you later, so it’s worth sorting out up front.
Designing the output block
if [ -n "$result" ]; then
echo "=== TypeScript型エラー検出 ==="
echo "$result"
echo "================================"
fi
exit 0
Enter fullscreen mode Exit fullscreen mode
The hook “prints nothing” when there are zero errors — that is, the normal case. If all types pass, stdout is empty and no noise lands in the agent’s log. Only when there are errors does it print with the === TypeScript型エラー検出 === prefix, so the agent can determine whether type errors exist simply by checking whether that string appears in the output.
-n "$result" checks that the string is non-empty. With no type errors, tsc‘s output is an empty string, so this condition works as the equivalent of “type errors present = output present.”
The 2>&1 | head -30 combination
result=$($TIMEOUT_CMD npx tsc $TSC_ARGS 2>&1 | head -30)
Enter fullscreen mode Exit fullscreen mode
2>&1 handles the case where tsc writes errors to stderr. TypeScript errors normally go to stderr. Without merging them into stdout, $result gets nothing. head -30 limits output to the top 30 lines so the buffer doesn’t overflow when errors are explosively numerous (cascading type errors producing hundreds of lines is not unusual).
Is there a basis for the number 30? There is. Looking at actual error logs from closet-os, the core of the error content (filename, line number, message) is concentrated in the first few lines. Thirty lines was enough for the agent to make a fix decision. Too many, and a single error consumes hundreds of tokens, degrading the hook’s cost efficiency.
Why use npx tsc
I use npx tsc rather than calling ./node_modules/.bin/tsc directly. The reason is simple: even when node_modules doesn’t exist (before the first npm install), npx falls back to a global tsc. The hook needs to run regardless of the project’s setup state. When node_modules does exist, npx prefers the local one, so there’s no version discrepancy.
Total line count of the script
The actual file is 61 lines. Excluding comments and blank lines, it’s effectively under 40. “The clarity of 100 lines over 1,000” is something you can embody in places like this. The shorter the functional core, the easier it is to copy-paste when porting to another project later.
Where I Got Stuck
Everything below is a failure I actually hit. Symptom, cause, fix — in that order.
① Adding a pipe made rc always 0
Symptom: tsc is emitting type errors, but no error message ever comes back from the hook. Adding echo "$result" for debugging shows the output is captured correctly, yet rc stays 0.
Cause: The first implementation was written like this.
result=$(npx tsc $TSC_ARGS 2>&1 | head -30)
rc=$?
Enter fullscreen mode Exit fullscreen mode
$? captures the exit code of the immediately preceding command. In a pipeline, $? returns the exit code of head. head -30 always succeeds (exit code 0), so no matter how many errors tsc emitted, rc=0 persisted.
Fix: I considered avoiding the pipe by capturing all output first and then running it through head, but that has buffer problems with massive errors. Currently the variables are two-tiered (result holds the head-filtered output, and rc looks at the exit code of the timeout command rather than immediately after the pipe). If you look at the actual script, rc=$? comes after the whole assignment, not after | head -30. As shell behavior goes, the exit code of a command substitution $(...) is that of the last command in the pipeline. timeout / gtimeout returns 124 only on timeout and otherwise passes tsc‘s exit code through transparently. In other words, by inserting timeout, the only thing inside the pipe is head, and timeout carries out tsc‘s code.
result=$($TIMEOUT_CMD npx tsc $TSC_ARGS 2>&1 | head -30)
rc=$?
# この rc は timeout コマンド全体の終了コード
# timeout が tsc を wrap しているので、tsc の rc が透過される
# (ただし timeout 自体の rc=124 がタイムアウトを示す)
Enter fullscreen mode Exit fullscreen mode
I wasn’t using timeout at first, which is how I fell into this trap. I only realized afterward that adding timeout had also become a side solution to the pipe problem.
② Getting the size failed with macOS stat
Symptom: I implemented the 200KB guard, but USE_INCREMENTAL=1 never changes regardless of state. The tsbuildinfo has clearly grown to several MB, yet no fallback.
Cause: I’d been reading Linux command references and wrote stat -c %s.
# 間違い(Linux 用)
SIZE=$(stat -c %s "$TSBUILDINFO" 2>/dev/null || echo 0)
Enter fullscreen mode Exit fullscreen mode
macOS’s stat is BSD-based and the options are completely different. -c %s is treated as an invalid option on macOS and returns an error. Since 2>/dev/null discards the error, echo 0 runs, and the size was always judged to be 0.
Fix: The macOS syntax is -f %z.
SIZE=$(stat -f %z "$TSBUILDINFO" 2>/dev/null || echo 0)
Enter fullscreen mode Exit fullscreen mode
-f specifies a format string, and %z returns the file size in bytes. Same meaning as Linux’s -c %s, different flag.
Discovering this required reading man stat and cost me 30 minutes. I’ve thought I should have just used wc -c < "$TSBUILDINFO" from the start, but wc -c reads the whole file, which is slightly slower on large files, and it needs extra error handling when the file doesn’t exist. stat only reads filesystem metadata so it’s faster, and the 2>/dev/null || echo 0 pattern handles absence safely in one line.
③ exit 1 stopped the agent
Symptom: Every time the hook detects a type error and runs, Claude Code’s tool execution loop gets interrupted midway. I can see errors are being emitted, but the loop ends before the agent starts reasoning “let me fix this.”
Cause: The first implementation returned exit 1 on type errors. When a PostToolUse hook returns a non-zero exit code, Claude Code treats the tool execution as “failed,” and there are cases where it doesn’t continue subsequent steps.
Fix: I switched to a design that limits the hook’s role to “information notification,” not “flow control.” Type errors are input information for the agent, not a reason to stop processing. By writing the error content to stdout and returning exit 0, the agent receives the information while continuing, and proceeds autonomously to the next action: “fix this type error.”
Once I reframed it as “the hook is an information channel, not a control channel,” the design cleaned right up.
④ ANSI escape codes polluted the agent’s context
Symptom: The agent should be receiving the type error log, but it can’t read the error content accurately and produces off-target fixes. When I paste the error message into my own terminal, mysterious strings like ^[[1m^[[31merror^[[0m are mixed in.
Cause: There was a period when I hadn’t added --pretty false. TypeScript’s default is colored output, which shows as red error text in a terminal, but the substance is ANSI escape sequences. When these get mixed into the hook’s output, the text the agent receives differs from what a human reads. LLMs don’t interpret escape sequences reliably, and control codes wedged into “the position where the word error should be” were blocking recognition.
Fix:
TSC_ARGS="--noEmit --pretty false ..."
Enter fullscreen mode Exit fullscreen mode
Just add --pretty false to the arguments. This makes tsc’s output plain text. A good rule to remember: always add --pretty false in hooks and CI environments. Terminal output is for humans to read; program-processed output doesn’t need it.
⑤ Concurrent launches corrupted .tsbuildinfo
Symptom: After using the hook for a while, tsc suddenly started emitting Cannot read file 'node_modules/.cache/tsc-hook.tsbuildinfo'. Deleting the file and retrying fixes it, but it breaks again a few hours later.
Cause: Claude Code sometimes executes multiple tools in parallel. When the agent issues “write file A” and “write file B” simultaneously, both PostToolUse hooks launch at the same time. When two tsc --incremental processes try to write to the same tsc-hook.tsbuildinfo file concurrently, the file ends up corrupted in a half-written state.
Fix: The current script doesn’t include full mutual exclusion (flock etc.). Instead, the policy is the 60-second timeout: “if multiple tsc processes run for a long time, let the OS reap them.” The comment says as much: # 並行 hook 実行による多重 tsc を防止 (prevent multiple tsc from concurrent hook execution).
The perfect solution is to use flock.
(
flock -x 200
# tsc 実行
) 200>"$TSBUILDINFO.lock"
Enter fullscreen mode Exit fullscreen mode
But adding this means more verification on macOS (BSD flock and GNU flock behave differently). In current operation, if .tsbuildinfo gets corrupted you delete it and it regenerates automatically next time, so I tolerate the loose timeout-based mitigation. It’s a judgment call that prioritizes operational simplicity over precision.
Even if tsbuildinfo breaks, Claude Code itself doesn’t stop. If the hook returns without type errors, it proceeds to the next step. At worst the impact is limited to “type checking is skipped for that one run.” It’s a concrete example of the principle that you don’t need to complicate code for non-fatal errors.
Stumbling Points (Real Landmines from Setup to Operation)
The middle section covered five: “pipe makes rc always 0,” “macOS stat syntax difference,” “exit 1 stopped the agent,” “ANSI code pollution,” and “tsbuildinfo corruption from concurrent launches.” Here’s a rapid-fire list of everything else that’s easy to trip over.
Forgetting
chmod +xmakes the hook silently ignoredJust placing the script doesn’t make it run. Until you grant execute permission with
chmod +x ~/.claude/hooks/post_tsc_check.sh, Claude Code quietly skips the hook. No error or warning appears, so you sit in a state of “I configured the hook but no type check comes back.” To verify, just runls -la ~/.claude/hooks/and check it showsrwxr-xr-x. This is the most common cause of getting stuck on day one.A
#!/bin/shshebang means bash syntax won’t parse in some placesThe
${SIZE:-0}parameter expansion andcommand -vwork in/bin/shtoo, but the moment you add bash-only syntax while extending the script, it breaks. That’s why the first line of the actual file is#!/bin/bash. It’s safest to use#!/bin/bashfrom the start.If the current directory isn’t the project root, the tsconfig.json check misses
PostToolUse hooks launch in the cwd at the time the agent executed the tool. If the agent is editing
packages/api/src/foo.tsand the cwd ispackages/api/, no problem — but if it’s still at the root, it can’t find the subpackage’stsconfig.jsonand exits immediately withexit 0. In a monorepo layout you need to reinforce this either by specifying the expected tsconfig path explicitly with--project, or by doingcd "$(git rev-parse --show-toplevel)"at the top of the hook to move to the root before running.In a monorepo,
tscpicks up an unexpected tsconfig.jsonWith multiple
tsconfig.jsonfiles scattered underpackages/, which one gets referenced changes with the cwd. Cases arise where you intended to use the root tsconfig but a subpackage’s tsconfig gets picked up, shifting the scope of type error detection. Two options: specify an absolute path inTSC_ARGSlike--project $(pwd)/tsconfig.json, or build a separate monorepo-aware hook.Dropping
--noEmitgenerates js files on every hook runIf you delete
--noEmitwhile manually tweaking the incremental arguments,tscwrites out.jsfiles. Build artifacts get overwritten on every hook, pollutinggit status. Worse, the agent can enter an infinite loop: “file changed → hook fires → js generated → file changed…” Keep--noEmitfixed at the front of the argument template.Whitespace in the
TSBUILDINFOpath splits the argumentIf quotes are missing from
--tsBuildInfoFile $TSBUILDINFO, the moment the project path contains a space, the argument passed totscsplits and errors out. If you’re using the script as-is without quotes, in an environment where the path contains spaces you need to add double quotes:--tsBuildInfoFile "$TSBUILDINFO".npxstartup cost piles up in high-frequency sessionsnpx tscresolves the existence ofnode_modules/.bin/tscbefore every run. The resolution itself is around 0.1–0.3 seconds, but when the agent edits dozens of files in one session, it adds up. In environments wherenode_modulesdefinitely exists, you have the option of pathing directly to./node_modules/.bin/tsc. But that reduces resilience for the initial setup whennode_modulesis absent, so it’s not suited to scripts shared with CI.Neither
gtimeoutnortimeoutexists in the CI environmentMinimal images like Alpine Linux sometimes don’t include
timeout.TIMEOUT_CMD=""stays empty andtscruns with no 60-second cutoff. CI type checks are usually managed in a dedicated job, but if you use the same script in CI, installtimeoutbeforehand with something likeRUN apk add --no-cache coreutils.head -30truncates errors midway and the agent loses sight of the root causeIn projects with cascading type errors, the core is concentrated in the first 30 lines, but in cases like circular reference errors where “the root cause is further down,” 30 lines can fall short. In that case, an effective approach is to combine it with an auxiliary hook that leaves the hook output in
/tmp/tsc-last.logviateeand has the agent read the full thing withcat /tmp/tsc-last.log. Keep the hook body simple, and supplement through a separate channel when information runs short — a separation-of-concerns mindset.Applying the 200KB threshold uniformly to every project
200KB is a value derived from measurements on one specific project,
closet-os. Change the project size, dependency tree depth, or machine specs, and the inversion point changes too. Get the real numbers for your own project with the following commands.
ls -lh node_modules/.cache/tsc-hook.tsbuildinfo # キャッシュの現サイズ
time npx tsc --noEmit # 通常モードの実測
time npx tsc --noEmit --incremental \
--tsBuildInfoFile node_modules/.cache/tsc-hook.tsbuildinfo # インクリメンタルの実測
Enter fullscreen mode Exit fullscreen mode
Take the cache size at the point where second-and-later incremental runs become “slower than normal mode” as your threshold, and you’ll have a boundary value specific to your project.
Best Practices
Here are the “I wish I’d done this from the start” principles that solidified through repeated implementation and failure.
1. Write the tsconfig.json check on line one
[ -f "tsconfig.json" ] || exit 0
Enter fullscreen mode Exit fullscreen mode
This is the defensive line that brings hook launch cost to zero in non-TypeScript projects. It’s the precondition for everything that follows, so it always goes on the first line.
2. Never remove --noEmit --pretty false — they’re a set
--noEmit prevents file generation, and --pretty false eliminates ANSI escape codes. Hook output becomes agent input, so machine-processed text doesn’t need color codes. Remove either of these two options from the argument template and both quietly cause problems.
3. Always return exit 0
The hook’s job is “to pass information,” not “to stop processing.” The moment you return a type error with exit 1, the hook turns from an inspection tool into an obstacle. Write the error content to stdout and return exit 0, and the agent receives the information while proceeding autonomously into a fix loop.
4. Use 2>&1 | head -30 as a set
tsc errors go to stderr. Without merging them into stdout via 2>&1, $result gets nothing. head -30 is buffer protection for when errors explode into hundreds of lines. Either one alone is incomplete.
5. Check for timeout in the order gtimeout → timeout → none
TIMEOUT_CMD=""
if command -v gtimeout >/dev/null 2>&1; then
TIMEOUT_CMD="gtimeout 60"
elif command -v timeout >/dev/null 2>&1; then
TIMEOUT_CMD="timeout 60"
fi
Enter fullscreen mode Exit fullscreen mode
macOS has no /usr/bin/timeout. Check with command -v in this order before assigning, and the same script runs on Linux, macOS, and CI.
6. Treat rc=124 separately from type errors
The exit code when timeout/gtimeout kills the process is 124. This is the information “tsc didn’t stop,” which means something different from a type error. By printing a dedicated message and returning exit 0, you can tell the agent about suspected concurrent launches while keeping the flow going.
7. Pin .tsbuildinfo to node_modules/.cache/
TSBUILDINFO="node_modules/.cache/tsc-hook.tsbuildinfo"
Enter fullscreen mode Exit fullscreen mode
Putting it in the project root increases .gitignore maintenance. node_modules/.cache/ is already excluded in most projects, so no extra configuration is needed. Giving the file a distinctive name like tsc-hook to separate its namespace from the app’s own build cache is also important.
8. Measure the 200KB guard threshold in your own project before setting it
Check the current cache size with stat -f %z, compare time npx tsc against time npx tsc --incremental, and find the inversion point yourself. Don’t reuse the magic number; put your measured boundary value in place of 204800 (200KB).
9. Use -f %z for macOS stat
Linux’s -c %s is invalid on macOS. Combined with 2>/dev/null || echo 0, you get the zero fallback for a missing file in one line. If you need both Linux and macOS support, wc -c < also works, but it reads the whole file on large files, so stat is slightly faster.
10. Always verify with a manual run after installing
cd /path/to/your/project
bash ~/.claude/hooks/post_tsc_check.sh
Enter fullscreen mode Exit fullscreen mode
Run it manually before going through Claude Code. Permissions, paths, stat options, tsconfig.json detection — this one command surfaces all of them. It’s vastly faster than debugging through Claude Code. Once confirmed clean, enable it as the production hook.
11. Temporarily disable the hook around big dependency changes
Right after adding a new library to package.json, tsc runs a full analysis and the hook blocks for 30–60 seconds. In those moments the right call is to temporarily disable it with chmod -x ~/.claude/hooks/post_tsc_check.sh, verify yourself with npm install && npx tsc --noEmit, and then restore chmod +x. Rather than clinging to full automation, the flexibility to toggle the hook by situation is what keeps operations stable.
12. Keep the script within 61 lines
The actual file is 61 lines including comments. Line count grows every time you add a feature, but if you hold the responsibility boundary of “the hook’s job is only to check and report,” it won’t bloat. If processing gets complex, that’s a sign to extract it into a separate dedicated script rather than the hook. “The clarity of 100 lines over 1,000” is achievable at this scale.
Summary
What I built is a 61-line shell script. That alone changed the closet-os hook execution time from “3.6x slower cold” to “1–3 seconds on diffs.”
The takeaways fit in three lines.
-
--incrementalbackfires on large projects. The size of.tsbuildinfois the indicator, and oncloset-osthe inversion point was around 200KB. -
A one-line guard that automatically falls back to normal mode when the size from
stat -f %zexceeds204800(200KB) solves it. -
The hook is an “information channel,” not a “control channel.” Always return
exit 0, and pass type errors to the agent via stdout.
With this structure running, every time Claude Code introduces a type error, the “=== TypeScript型エラー検出 ===” feedback comes back and the agent autonomously runs the fix loop. The lag for a human to notice a type error is zero. It’s one of the unglamorous parts supporting a ¥1.2M-a-month autonomous setup, but precisely because it’s unglamorous, it has kept running stably for months.
If you’re feeling that --incremental is heavy on a project around the size of closet-os right now, start by running ls -lh node_modules/.cache/tsc-hook.tsbuildinfo. If it’s over 200KB, you can fix it today.
Where does the inversion point land on your project — and what size does your .tsbuildinfo hit before incremental stops paying off?
The full picture of the setup, the breakdown of the ¥1.2M/month, and a 30-day walkthrough are in a paid note (Japanese)
📕 Claude Code自律環境で、実際どう稼ぐか ― 仕組み・実例・始め方・サポート
Written by **Lily* — I ship iOS apps and automate my content stack with Claude Code.
Follow along: Portfolio · X · GitHub*