[BUG] /goal Stop hook emits markdown to stdout, fails JSON schema validation ("JSON validation failed")
Summary
The internal Stop hook spawned by the /goal slash command writes a human-readable markdown report to stdout instead of a JSON payload conforming to the Stop hook output schema. Claude Code logs stderr: \"JSON validation failed\" and exitCode: 1, surfacing as Stop hook error: JSON validation failed in the UI on every /goal evaluation.
The error is non-blocking: the goal auto-clears correctly and the user's work is unaffected. But the noisy error message appears repeatedly during normal use of /goal.
Environment
- Claude Code:
2.1.140 - Node:
24.15.0(volta-managed) - Platform: Linux (WSL2 Ubuntu)
- User-defined Stop hooks present (atlas + plugins) — verified independently OK
- 4 total Stop hooks reported (
Ran 4 stop hooks)
Reproduction
- Issue any
/goal <condition>slash command. - Complete the work that satisfies the condition.
- Observe: \"Ran N stop hooks\" + \"Stop hook error: JSON validation failed\".
- Despite the error, the goal auto-clears (condition correctly evaluated as satisfied).
Evidence
The session transcript JSONL records the actual hook payload as a hook_non_blocking_error attachment:
{
\"type\": \"attachment\",
\"attachment\": {
\"type\": \"hook_non_blocking_error\",
\"hookName\": \"Stop\",
\"hookEvent\": \"Stop\",
\"toolUseID\": \"<uuid>\",
\"stderr\": \"JSON validation failed\",
\"stdout\": \"<markdown-evaluation-report>\",
\"exitCode\": 1,
\"command\": \"<full-text-of-the-goal-condition>\",
\"durationMs\": 11656
}
}
The command field exactly matches the text passed to /goal. The stdout field contains markdown like:
# Evaluation of Stop Condition: <name>
## Answer: YES, condition is satisfied
## Evidence from Transcript
...
**Condition fully satisfied. Stop hook can auto-clear.**
This is human-readable evaluation content — not a JSON object matching the documented Stop hook output schema ({decision, reason, systemMessage, continue, suppressOutput, hookSpecificOutput}).
Diagnosis
I verified the four user-/plugin-level Stop hooks individually with isolated dry-runs (echo '{...}' | node hook.js):
| Hook | Source | exit | stdout |
|------|--------|------|--------|
| echo STOP_HOOK_OK >> /tmp/... | ~/.claude/settings.json | 0 | empty |
| atlas-session-stop.js | ~/.claude/settings.json | 0 | empty |
| openai-codex/stop-review-gate-hook.mjs | plugin | 0 | empty (no review needed) |
| thedotmack/claude-mem summarize | plugin | 0 | {} |
All four are conformant. The failing hook is the internal /goal evaluator hook, which is not present in settings.json or any plugin manifest — it is generated and dispatched by Claude Code itself when a /goal is active.
Root Cause Hypothesis
The agent/subagent that evaluates the /goal stop condition is prompted to assess whether the condition holds, but the prompt template does not constrain output to JSON. The model responds with a natural-language markdown report. Claude Code then attempts JSON.parse(stdout), fails, records JSON validation failed to stderr, and downgrades the failure to hook_non_blocking_error (exitCode 1) so the session continues.
Expected Behavior
Either:
- The internal
/goalevaluator should emit a Stop hook output payload conforming to the schema, e.g.:
``json``
{\"decision\": \"approve\", \"reason\": \"Condition satisfied: ...\"}
- Or, Claude Code should not validate stdout from internal hooks against the same schema as user hooks (or should treat empty/markdown stdout as silent OK).
Suggested Fix
- Update the
/goalevaluator prompt template to require structured JSON output matching the Stop hook output schema. - Alternatively, wrap the evaluator output in a JSON envelope before exposing it to the hook validator (e.g.
{\"decision\": \"approve\", \"reason\": <markdown-as-string>}). - Or, suppress the \"JSON validation failed\" surfacing for internal
/goalhooks — at minimum it should not appear as a user-facing error when the underlying logic succeeds.
Impact
- Severity: cosmetic / noisy.
- Functional impact: none —
/goalauto-clear works correctly. - User impact: misleading error messages on every goal evaluation, eroding trust in the hook system and obscuring real hook errors.
Notes
- I have not modified anything locally to reproduce this; the diagnosis is based on reading the transcript JSONL captured during a regular
/goalsession. - Happy to provide additional sanitized payloads or transcripts if useful.
This issue has 4 comments on GitHub. Read the full discussion on GitHub ↗