perf: skip the jq spawn on non-skill reads - #6
Open
learte4 wants to merge 1 commit into
Open
Conversation
The hook is registered for every Read, but only reads under .claude/skills/ produce a log entry — in practice well under 1% of calls. The current code still spawns jq to parse the payload before it can decide that, so the subprocess is paid on every read and thrown away almost every time. Move the decision ahead of the parse with a plain shell pattern match on the raw stdin. The pattern covers both separator styles, so JSON-escaped Windows paths still match, and it is strictly wider than the file_path guard that follows — anything that used to be logged still is. Measured on Windows (Git Bash, 25 runs, non-skill read): 538 ms -> 188 ms per invocation. The remainder is bash startup, which a `type: command` hook cannot avoid. On macOS/Linux the saving is smaller in absolute terms but the spawn is removed all the same. Verified with a throwaway harness that puts a failing `jq` first on PATH: a non-skill read exits 0 without ever calling it, a skill read still logs the event, and a JSON-escaped Windows path still reaches the parser. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The hook is registered for
Read, so it runs for every file the model opens — but only reads under.claude/skills/produce a log entry, which is a small fraction of them. On the machine where I noticed this, one project's log had 23 entries across three weeks while the Read tool fired several thousand times.The problem is the ordering:
main()spawnsjqto parse the payload before it can tell a skill read from an ordinary one. So the subprocess is paid on every read and thrown away almost every time. The comment at the top of the file already promises "Fast-exits for non-skill reads" — this just makes the exit happen early enough to be worth the name.Change
Decide ahead of the parse, with a plain shell pattern match against the raw stdin:
The second pattern covers JSON-escaped Windows paths, so nothing is dropped on that platform. The match is strictly wider than the
file_pathguard that follows: any payload that used to reach the logging code still reaches it, and the guard still has the final say. Worst case a payload mentions the path in some other field, passes here, and exits at the existing guard — the behavior you have today.Measurement
Git Bash on Windows 11, 25 invocations with a non-skill read payload:
The remaining 188 ms is
bashstartup, which atype: commandhook can't avoid. Windows is the extreme case because process creation is expensive there; on macOS and Linux the absolute saving is smaller, but the spawn goes away all the same.How I verified it
There's no test harness in the repo, so I used a throwaway one: put a failing
jqfirst onPATH, then feed the script three payloads.jq(the fake would have failed loudly).jq→ still appends the event,"skill":"ponytail".All three pass. Happy to add this under
tests/if you'd like a permanent version — I left it out because the repo doesn't have a test layout yet and that felt like your call rather than mine.verification harness
Independent of #5, which fixes an unrelated Windows bug a few lines further down in the same function. Whichever lands first, the other needs at most a trivial rebase — I'm happy to do it.
Note that on Windows today, #5 is what makes the logging work at all; this PR only changes how much it costs to decide not to log.