test-hook.sh reports valid JSON as invalid when jq is not installed
When jq is not installed, plugin-dev/skills/hook-development/scripts/test-hook.sh reports that the test input is invalid JSON. The input is valid. The script fails closed with a wrong diagnosis, which sends the user to debug their input instead of installing the missing dependency.
Root cause
Line 155:
if ! jq empty "$TEST_INPUT" 2>/dev/null; then
echo "❌ Error: Test input is not valid JSON"
2>/dev/null discards the shell's jq: command not found, so a missing binary is indistinguishable from a parse failure.
Reproduction
On a machine without jq (common on Windows):
$ node -e "JSON.parse(require('fs').readFileSync('input.json','utf8')); console.log('valid')"
valid
$ bash test-hook.sh guard.mjs input.json
❌ Error: Test input is not valid JSON
$ echo $?
1
Expected
A missing dependency should be reported as a missing dependency.
Suggested fix
Guard on availability before use:
if ! command -v jq >/dev/null 2>&1; then
echo "Error: jq is required but not installed. See https://jqlang.github.io/jq/"
exit 1
fi
Worth noting that references/hooks.md in the Claude Code docs already warns against depending on jq inside hook handlers for this exact reason ("jq is absent on many Windows installs"). The same caution applies to the tooling that tests those handlers.
Environment: Claude Code 2.1.219, Windows 11, Git Bash, Node 24.14.1, no jq on PATH.
4 Comments
Correction to my own report, please disregard the final paragraph.
I wrote that "
references/hooks.mdin the Claude Code docs already warns against depending onjqinside hook handlers". That attribution is wrong and I withdraw it.references/hooks.mdpage in the Claude Code documentation. The sentence I quoted is from a reference file in my own project, and my own evidence ledger records it as unpublished own-work research, not as a documented statement from this project.jq, so installjqand make sure it is on yourPATHbefore trying them."So
jqis a deliberate and documented dependency, and I should not have presented my own note as your guidance. Apologies for the noise.The reported defect itself is unaffected, and I have re-checked it. With
jqabsent,test-hook.shline 155 reports:for input that
JSON.parseaccepts, becausejq empty "$TEST_INPUT" 2>/dev/nulldiscards the shell'scommand not found. The suggested fix stands: check for the dependency explicitly so a missing binary is reported as a missing binary rather than as malformed input.Thanks for the correction and for re-checking the behavior.
That clarification makes sense. jq is an intentional and documented dependency for these hook examples. The proposed change does not remove or replace that dependency; it only makes the failure mode accurate when the executable is missing.
I independently reproduced the remaining defect. When jq is absent, valid JSON is reported as invalid because the command jq empty "$TEST_INPUT" 2>/dev/null conflates a missing command with a JSON parsing failure.
The fix adds an explicit availability check so users receive:
Error: jq is required but was not found on PATH
Malformed JSON still receives the existing invalid-JSON diagnostic.
The verified implementation and regression test are available here:
https://github.com/PranshulSoni/claude-code/tree/fix/plugin-dev-missing-jq-diagnostic
The commit is:
7c46dd4ecaf397c690bff21d800b65a3ea09bde4
My account can push to the fork, but GitHub does not allow it to create pull requests against anthropics/claude-code. A maintainer can open a pull request from the branch or cherry-pick the commit.
Reproduced. With a syntactically valid JSON test input and
jqabsent fromPATH,test-hook.shreports "❌ Error: Test input is not valid JSON" and exits 1; the identical invocation withjqinstalled proceeds normally. The script redirects stderr to /dev/null around thejqvalidation call, so "command not found" is indistinguishable from a real parse failure and gets misreported as invalid input. The suggested up-front dependency check (fail with a clear "jq is required but not installed" message) is the right shape of fix, and the same missing-jq misdiagnosis would also affect the script's laterjqcalls. Thanks for the precise report and root cause.🤖 Generated with Claude Code
Branch with the fix, extended to cover the second script:
Diff: https://github.com/anthropics/claude-code/compare/main...Malwurf:claude-code:fix/jq-dependency-check
Commit: https://github.com/Malwurf/claude-code/commit/388092dec0bec62ad9acffea02645d828b30b3fc
Credit to @PranshulSoni, who root-caused this and published a fix first; they are the co-author on the commit. Two things added on top:
validate-hook-schema.sh:32has the identical bug and is not mentioned in the issue —if ! jq empty "$HOOKS_FILE" 2>/dev/nullprints❌ Invalid JSON syntaxon a valid file whenjqis absent. It is fixed here too.--help,--create-sampleand the usage output still work withoutjq, while every laterjqcall is covered — per @bcherny's note that the later calls are affected as well.Repo-wide sweep for the misdiagnosing pattern (
jqwith stderr silenced): these two files are the only occurrences.hook-linter.shnever invokesjq— its line 62 isgrep -q "jq" "$script", which inspects other people's scripts. The remainingjqcallers (.devcontainer/init-firewall.sh,ralph-wiggum/hooks/stop-hook.sh,scripts/*.sh) do not silence stderr, so a missingjqalready surfaces honestly there. Theexamples/*.shsnippets are left alone deliberately — they are meant to be copied into a user's own project.Verification
jqlives in/usr/binhere, so the missing-jqcase was reproduced with aPATHof symlinks to every binary in/usr/binand/binexceptjq. Input confirmed valid withpython3 -m json.tool.Before:
After:
With
jqpresent, output is byte-identical before and after for both scripts (diffed; the only delta istest-hook.sh'sDuration: Nswall-clock line).--help,--create-sample PreToolUseand the no-args usage all still work withoutjq.bash -npasses;shellcheck 0.11.0reports 0 findings ontest-hook.shand the same 12 pre-existing findings onvalidate-hook-schema.shbefore and after.@PranshulSoni also wrote a 36-line regression test. I left it out because the repo carries no test files anywhere and no workflow that would run one — happy to add it if you'd like it.
I could not open a pull request either:
CreatePullRequestreturnsMalwurf does not have the correct permissions. Cross-fork PRs fromauthor_association=NONEaccounts appear to have stopped on this repo after 2026-08-16, which matches what @PranshulSoni and #90065 report. Cherry-pick or pull from the branch above as you prefer.