Skip to content

SessionStart hook silently fails: invalid JSON format + Claude Code Bug #16538 — with fix and smart loading proposal #110

@wasikarn

Description

@wasikarn

Problem

The session-start.sh hook produces zero visible output in Claude Code sessions, even though the script executes successfully. The meta-skill using-agent-skills is silently discarded on every session start.

Step-by-Step Root Cause Analysis

Layer 1: Invalid Output Format (Primary)

The hook outputs an undocumented JSON format that Claude Code does not recognize:

{"priority": "IMPORTANT", "message": "agent-skills loaded...\n\n[FULL SKILL.MD]"}

Why this fails:

  1. The official Claude Code hooks reference (code.claude.com/docs/en/hooks.md) defines valid SessionStart hook output as:

    • Plain text stdout — any non-JSON text is added directly as context
    • hookSpecificOutput.additionalContext — structured JSON with specific schema
    • systemMessage — universal top-level field
  2. The "priority" / "message" top-level fields do not exist in the Claude Code hook schema. They are neither documented in the hooks reference nor in the agent-sdk hooks docs.

  3. When Claude Code receives output starting with { but not matching the valid schema, it attempts to parse it as JSON. The unrecognized fields are silently ignored, and no context is injected.

  4. This was verified by comparing with other plugins:

    • superpowers (obra/superpowers) uses valid hookSpecificOutput schema
    • oh-my-claudecode uses plain text with budget guards
    • claude-mem uses {"continue":true,"suppressOutput":true} (valid universal fields)

Layer 2: Plugin Pipeline Bug (Secondary, Upstream)

Even if the format were fixed, Claude Code has a known upstream bug:

  • Bug #16538 (anthropics/claude-code#16538): Plugin-defined SessionStart hooks that output hookSpecificOutput.additionalContext have their context silently dropped. Native hooks defined in ~/.claude/settings.json work correctly.
  • Affected events: SessionStart, PreCompact
  • Working events: PostToolUse, UserPromptSubmit

This means even a correctly-formatted hookSpecificOutput from the plugin's hooks/hooks.json would still fail.

Verification Steps Performed

Test Result
Manual execution of session-start.sh ✅ Script runs, outputs JSON
Plugin enabled in settings.json agent-skills@addy-agent-skills: true
Hook auto-merges from plugin ✅ Confirmed via /hooks
Output visible in session context ❌ Zero appearance
Native plain-text hook test ✅ Works when defined in settings.json
Native broken-JSON hook test ❌ Silent failure (confirms format is root cause)

The Fix (What We Implemented)

1. Change Output Format

Replaced the invalid {"priority":"...","message":"..."} format with plain text stdout:

#!/bin/bash
# ...

if [ -f "$META_SKILL" ]; then
  echo "agent-skills loaded. Project context detected. Most relevant skills are primed below."
  echo ""
  cat "$META_SKILL"
else
  echo "agent-skills: meta-skill not found."
fi

2. Make the Hook "Smart"

Instead of dumping the entire 8.6KB SKILL.md into every session, the hook now uses tiered loading:

  • Tier 1: Universal Core — always loaded (Core Operating Behaviors, Failure Modes, Skill Discovery Flowchart)
  • Tier 2: Project Detection — detects project type from repo files (package.json, Cargo.toml, etc.)
  • Tier 3: Skill Mapping — maps project type to 3-5 most relevant skills
  • Tier 4: Primer Extraction — loads only description + overview + "When to Use" from relevant skills
  • Tier 5: Budget Guard — hard limit at 6000 chars (inspired by oh-my-claudecode's SESSION_START_CONTEXT_BUDGET)

Example: For a docs/wiki project → primed: documentation-and-adrs, context-engineering

3. Bypass Plugin Pipeline Bug

To avoid Bug #16538:

  1. Removed SessionStart from plugin's hooks/hooks.json:

    {"hooks": {}}
  2. Added native hook in ~/.claude/settings.json:

    "SessionStart": [
      {
        "matcher": "",
        "hooks": [
          {
            "type": "command",
            "command": "bash $(cd ~ && pwd)/.claude/plugins/cache/addy-agent-skills/agent-skills/1.0.0/hooks/session-start.sh",
            "timeout": 10
          }
        ]
      }
    ]

    Important: Used $(cd ~ && pwd) instead of ${HOME} because ${HOME} is not set in Claude Code hook environments.

  3. Disabled auto-update to prevent the fix from being overwritten:

    "extraKnownMarketplaces": {
      "addy-agent-skills": {
        "source": {"source": "github", "repo": "addyosmani/agent-skills"},
        "autoUpdate": false
      }
    }

Suggested Fix for Upstream

Option A: Merge the Smart Hook (Recommended)

Replace hooks/session-start.sh with the smart version that:

  1. Uses plain text output (valid Claude Code format)
  2. Detects project type dynamically
  3. Loads only relevant skill primers
  4. Includes budget guard
  5. Uses $(cd ~ && pwd) instead of ${HOME}

Option B: Minimal Fix (Quick)

If keeping the current architecture:

  1. Change output from {"priority":"...","message":"..."} to plain text
  2. Keep dumping full SKILL.md (accept the token cost)
  3. Document that users must add the hook natively in settings.json to bypass Bug #16538

Option C: Use hookSpecificOutput.additionalContext

jq -nc --arg ctx "$CONTENT" '
  {
    hookSpecificOutput: {
      hookEventName: "SessionStart",
      additionalContext: $ctx
    }
  }'

Note: This still requires the native-hook workaround because Bug #16538 affects all plugin-defined SessionStart hooks.

Environment

  • Claude Code versions tested: v2.1.39 – v2.1.86+ (issue confirmed across versions)
  • Platform: macOS (darwin) — sed portability was also a concern
  • jq installed: yes (jq-1.8.1)
  • Plugin enabled: yes
  • Hook executes: yes (verified manually and in debug logs)
  • Output visible in session: no (before fix) / yes (after fix)

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions