[BUG] /goal Stop hook emits markdown to stdout, fails JSON schema validation ("JSON validation failed")

Open 💬 4 comments Opened May 25, 2026 by auriti

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

  1. Issue any /goal <condition> slash command.
  2. Complete the work that satisfies the condition.
  3. Observe: \"Ran N stop hooks\" + \"Stop hook error: JSON validation failed\".
  4. 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:

  1. The internal /goal evaluator should emit a Stop hook output payload conforming to the schema, e.g.:

``json
{\"decision\": \"approve\", \"reason\": \"Condition satisfied: ...\"}
``

  1. 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 /goal evaluator 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 /goal hooks — at minimum it should not appear as a user-facing error when the underlying logic succeeds.

Impact

  • Severity: cosmetic / noisy.
  • Functional impact: none — /goal auto-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 /goal session.
  • Happy to provide additional sanitized payloads or transcripts if useful.

View original on GitHub ↗

This issue has 4 comments on GitHub. Read the full discussion on GitHub ↗