claude-code - 💡(How to fix) Fix [BUG] /goal Stop hook emits markdown to stdout, fails JSON schema validation ("JSON validation failed")

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…

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.

Error Message

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. 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).

  • 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.
  • User impact: misleading error messages on every goal evaluation, eroding trust in the hook system and obscuring real hook errors.

Root Cause

Root Cause Hypothesis

Fix Action

Fix / Workaround

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.

Code Example

{
  \"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
  }
}

---

# Evaluation of Stop Condition: <name>

## Answer: YES, condition is satisfied

## Evidence from Transcript
...

**Condition fully satisfied. Stop hook can auto-clear.**

---

{\"decision\": \"approve\", \"reason\": \"Condition satisfied: ...\"}
RAW_BUFFERClick to expand / collapse

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):

HookSourceexitstdout
echo STOP_HOOK_OK >> /tmp/...~/.claude/settings.json0empty
atlas-session-stop.js~/.claude/settings.json0empty
openai-codex/stop-review-gate-hook.mjsplugin0empty (no review needed)
thedotmack/claude-mem summarizeplugin0{}

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.:
    {\"decision\": \"approve\", \"reason\": \"Condition satisfied: ...\"}
  2. 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.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

claude-code - 💡(How to fix) Fix [BUG] /goal Stop hook emits markdown to stdout, fails JSON schema validation ("JSON validation failed")