Skip to content

Bug: stop-hook.ts never captures learnings due to missing response field #303

@MarkoSteenbergen

Description

@MarkoSteenbergen

Bug Report: stop-hook.ts never captures learnings due to missing response field

Summary

The stop-hook.ts in kai-history-system pack v1.0.0 expects a response field in the Stop event payload, but Claude Code never sends this field. This causes the hook to exit immediately on line 80, preventing all learning and session captures from being saved.

Impact

  • Learnings are never captured - history/learnings/ remains empty
  • Sessions are never captured via stop-hook - Only SessionEnd hook creates summaries
  • Core feature of history system is broken - Auto-categorization by learning indicators doesn't work

Evidence

What Claude Code Actually Sends

From history/raw-outputs/YYYY-MM/YYYY-MM-DD_all-events.jsonl:

{
  "hook_event_type": "Stop",
  "payload": {
    "session_id": "8ab02882-5caf-4682-9f1e-2d3f059bd297",
    "transcript_path": "/home/marko/.claude/projects/-home-marko--config-pai/8ab02882-5caf-4682-9f1e-2d3f059bd297.jsonl",
    "cwd": "/home/marko/.config/pai",
    "permission_mode": "acceptEdits",
    "hook_event_name": "Stop",
    "stop_hook_active": false
  }
}

Note: No response field present.

What stop-hook.ts Expects

Packs/kai-history-system/src/stop-hook.ts lines 78-81:

const payload: StopPayload = JSON.parse(stdinData);
if (!payload.response) {
  process.exit(0);  // ❌ Always exits here
}

Root Cause

During the v2.0 refactor to directory-based structure (commit 1c34d06), the stop-hook was simplified from the working version that read transcripts. The older .claude/hooks/stop-hook.ts (commit 7b3031e) correctly handled this by:

1. Reading transcript_path from payload
2. Parsing the transcript JSONL file
3. Extracting the last assistant message

The simplified kai-history-system pack version removed this logic but forgot that Claude Code doesn't send a response field.

Reproduction Steps

1. Install kai-history-system pack v1.0.0
2. Complete any session with learning indicators (e.g., "I discovered the problem was X. The root cause was Y. After debugging, I fixed it by Z.")
3. Check $PAI_DIR/history/learnings/ - it will be empty
4. Check raw events log to confirm Stop hook was called but produced no output

Proposed Fix

Replace the response field check with transcript reading logic:

async function main() {
  try {
    const stdinData = await Bun.stdin.text();
    if (!stdinData.trim()) {
      process.exit(0);
    }

    const payload: StopPayload = JSON.parse(stdinData);

    // Read transcript to get the last assistant response
    if (!payload.transcript_path || !existsSync(payload.transcript_path)) {
      process.exit(0);
    }

    const transcriptContent = readFileSync(payload.transcript_path, 'utf-8');
    const lines = transcriptContent.trim().split('\n').filter(l => l.trim());

    if (lines.length === 0) {
      process.exit(0);
    }

    // Get last assistant message
    let response = '';
    for (let i = lines.length - 1; i >= 0; i--) {
      try {
        const entry = JSON.parse(lines[i]);
        if (entry.type === 'assistant' && entry.message?.content) {
          // Extract text from content array
          const contentArray = Array.isArray(entry.message.content)
            ? entry.message.content
            : [entry.message.content];

          response = contentArray
            .map(c => {
              if (typeof c === 'string') return c;
              if (c?.text) return c.text;
              if (c?.content) return String(c.content);
              return '';
            })
            .join('\n')
            .trim();

          if (response) break;
        }
      } catch (e) {
        continue;
      }
    }

    if (!response) {
      process.exit(0);
    }

    // ... rest of existing logic continues unchanged

Also add readFileSync to imports:

import { writeFileSync, mkdirSync, existsSync, readFileSync } from 'fs';

Test Results

After applying the fix and testing with real Stop event payloads:

Test 1: SESSION Capture

$ cat real-stop-payload.json | ~/.config/pai/hooks/stop-hook.ts
📝 Captured SESSION to sessions/2026-01/20260103T122209_SESSION_uitstekende-vraag.md

✅ File created with correct content extracted from transcript

Test 2: LEARNING Detection

$ cat learning-payload.json | ~/.config/pai/hooks/stop-hook.ts
📝 Captured LEARNING to learnings/2026-01/20260103T122240_LEARNING_discovered-problem-root-cause.md

✅ Learning indicators correctly detected (8 keywords: discovered, problem, root cause, debugging, realized, solution, fixed, bug)
✅ File saved to learnings/ directory instead of sessions/

Environment

- OS: Linux (Ubuntu/Debian-based)
- Claude Code: Latest version (as of 2026-01-03)
- PAI Version: Kai Bundle v2.0.0
- Pack Version: kai-history-system v1.0.0
- Bun Version: 1.x

Additional Notes

- This bug affects all installations of kai-history-system pack v1.0.0
- The bug exists in both the initial install and the current GitHub main branch
- Commit b6bd951 fixed session_id field mismatch but didn't address the response field issue
- No open issues on GitHub currently reference this problem

Suggested Immediate Action

Update Packs/kai-history-system/src/stop-hook.ts with the transcript reading logic from the older working version (.claude/hooks/stop-hook.ts @ commit 7b3031e), adapted for the current pack structure.

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