Skip to content

Phase 8 CCH Hook Integration

Rick Hightower edited this page Feb 1, 2026 · 1 revision

Phase 8: CCH Hook Integration

This page aggregates all Phase 8 documentation for the CCH Hook Integration phase.

Phase Overview

Automatic event capture via CCH hooks.


08-01-PLAN

Plan 08-01: CCH Hook Handler Binary

Phase: 8 - CCH Hook Integration Wave: 1 Depends on: Phase 7 complete, memory-client crate

Goal

Create a memory-ingest binary that CCH can invoke to automatically capture conversation events and send them to memory-daemon.

Context

The memory-client crate already provides:

  • HookEvent type with all CCH event types
  • map_hook_event() function for conversion
  • MemoryClient with ingest() for gRPC

This plan creates a thin CLI wrapper (~50 lines) that:

  1. Reads CCH JSON from stdin
  2. Converts to HookEvent
  3. Maps to memory Event
  4. Sends via gRPC
  5. Returns {"continue": true} to stdout

Tasks

Task 1: Create memory-ingest Crate

What: Set up new binary crate in workspace

Files:

  • crates/memory-ingest/Cargo.toml
  • crates/memory-ingest/src/main.rs

Cargo.toml:

[package]
name = "memory-ingest"
version = "0.1.0"
edition = "2021"

[[bin]]
name = "memory-ingest"
path = "src/main.rs"

[dependencies]
memory-client = { path = "../memory-client" }
memory-types = { path = "../memory-types" }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
tokio = { version = "1.43", features = ["rt"] }
anyhow = "1.0"
chrono = { version = "0.4", features = ["serde"] }

Acceptance Criteria:

  • Crate builds as part of workspace
  • Binary name is memory-ingest

Task 2: Implement CCH Event Parsing

What: Parse CCH JSON format from stdin

Implementation:

#[derive(Debug, Deserialize)]
struct CchEvent {
    hook_event_name: String,
    session_id: String,
    #[serde(default)]
    message: Option<String>,
    #[serde(default)]
    tool_name: Option<String>,
    #[serde(default)]
    tool_input: Option<serde_json::Value>,
    #[serde(default)]
    timestamp: Option<DateTime<Utc>>,
    #[serde(default)]
    cwd: Option<String>,
}

Acceptance Criteria:

  • Parses all CCH event types
  • Handles missing optional fields gracefully

Task 3: Implement Event Mapping

What: Convert CchEvent to HookEvent using existing types

Implementation:

fn map_cch_to_hook(cch: &CchEvent) -> HookEvent {
    let event_type = match cch.hook_event_name.as_str() {
        "SessionStart" => HookEventType::SessionStart,
        "UserPromptSubmit" => HookEventType::UserPromptSubmit,
        "PreToolUse" => HookEventType::ToolUse,
        "PostToolUse" => HookEventType::ToolResult,
        "Stop" => HookEventType::Stop,
        "SubagentStart" => HookEventType::SubagentStart,
        "SubagentStop" => HookEventType::SubagentStop,
        _ => HookEventType::UserPromptSubmit, // Default fallback
    };

    let content = cch.message.clone().unwrap_or_default();
    let mut hook = HookEvent::new(&cch.session_id, event_type, content);

    if let Some(ts) = cch.timestamp {
        hook = hook.with_timestamp(ts);
    }
    if let Some(tool) = &cch.tool_name {
        hook = hook.with_tool_name(tool);
    }

    hook
}

Acceptance Criteria:

  • Maps all CCH event types correctly
  • Preserves timestamp if provided
  • Includes tool_name in metadata

Task 4: Implement Main Function

What: Wire up stdin → parse → map → ingest → stdout

Implementation:

fn main() -> anyhow::Result<()> {
    // Read from stdin
    let stdin = std::io::stdin();
    let mut input = String::new();
    stdin.lock().read_line(&mut input)?;

    // Parse CCH event
    let cch: CchEvent = serde_json::from_str(&input)?;

    // Map to memory event
    let hook_event = map_cch_to_hook(&cch);
    let event = map_hook_event(hook_event);

    // Ingest via gRPC (fail open)
    let rt = tokio::runtime::Runtime::new()?;
    let _ = rt.block_on(async {
        if let Ok(mut client) = MemoryClient::connect_default().await {
            let _ = client.ingest(event).await;
        }
    });

    // Always return success to CCH
    println!(r#"{{"continue":true}}"#);
    Ok(())
}

Acceptance Criteria:

  • Reads single line from stdin
  • Fails open (returns success even if daemon down)
  • Outputs valid JSON to stdout

Task 5: Add to Workspace

What: Update root Cargo.toml to include new crate

Edit: Cargo.toml (workspace root)

[workspace]
members = [
    "crates/memory-daemon",
    "crates/memory-service",
    "crates/memory-client",
    "crates/memory-storage",
    "crates/memory-toc",
    "crates/memory-types",
    "crates/memory-ingest",  # Add this
]

Acceptance Criteria:

  • cargo build -p memory-ingest works
  • cargo build --release -p memory-ingest produces optimized binary

Task 6: Create Example hooks.yaml

What: Provide sample CCH configuration

File: examples/hooks.yaml

version: "1.0"

settings:
  fail_open: true
  script_timeout: 5

rules:
  - name: capture-to-memory
    description: Send conversation events to agent-memory daemon
    matchers:
      operations:
        - SessionStart
        - UserPromptSubmit
        - PostToolUse
        - SessionEnd
        - SubagentStart
        - SubagentStop
    actions:
      run: "~/.local/bin/memory-ingest"

Acceptance Criteria:

  • Example file is valid YAML
  • Comments explain configuration

Task 7: Write Tests

What: Unit tests for event mapping

Tests:

  • test_parse_session_start
  • test_parse_user_prompt
  • test_parse_tool_use
  • test_map_all_event_types
  • test_fail_open_behavior

Acceptance Criteria:

  • All tests pass
  • Coverage for all event types

Task 8: Update Documentation

What: Add installation and usage instructions

Update: docs/README.md - Add CCH Integration section

Acceptance Criteria:

  • Installation instructions
  • hooks.yaml configuration example
  • Troubleshooting section

Verification

# Build
cargo build --release -p memory-ingest

# Test with sample event
echo '{"hook_event_name":"UserPromptSubmit","session_id":"test-123","message":"Hello world"}' | ./target/release/memory-ingest
# Expected output: {"continue":true}

# Install
cp target/release/memory-ingest ~/.local/bin/

# Verify installation
~/.local/bin/memory-ingest --version

# Test with daemon running
memory-daemon start
echo '{"hook_event_name":"SessionStart","session_id":"test-123"}' | memory-ingest
memory-daemon query root  # Should show new events

Success Criteria

  1. Binary reads CCH JSON from stdin
  2. Binary outputs {"continue":true} to stdout
  3. Events are ingested when daemon is running
  4. Binary fails open when daemon is down (no hang, returns success)
  5. All CCH event types are mapped correctly
  6. Example hooks.yaml provided

Notes

  • Binary must be fast (<100ms) to not slow down Claude
  • Always fail open - don't block Claude if memory system is down
  • Use existing memory-client code - don't duplicate logic

08-01-SUMMARY


phase: 8 plan: 1 subsystem: integration tags: [cch, hooks, ingestion, cli]

dependency-graph: requires: [07-complete, memory-client] provides: [memory-ingest-binary, cch-integration] affects: []

tech-stack: added: [] patterns: [fail-open, stdin-stdout-protocol]

key-files: created: - crates/memory-ingest/Cargo.toml - crates/memory-ingest/src/main.rs - examples/hooks.yaml modified: - Cargo.toml - docs/README.md

decisions:

  • id: CCH-01 choice: fail-open behavior rationale: never block Claude even if memory system is down
  • id: CCH-02 choice: reuse memory-client types rationale: avoid duplication of HookEvent mapping logic
  • id: CCH-03 choice: minimal binary (~200 lines) rationale: fast startup, simple maintenance

metrics: duration: 4min completed: 2026-01-31

Phase 8 Plan 1: CCH Hook Handler Binary Summary

One-liner: Lightweight memory-ingest binary for CCH hook integration with fail-open semantics

What Was Built

memory-ingest Binary

A minimal Rust binary (crates/memory-ingest) that integrates with code_agent_context_hooks (CCH):

  1. Reads CCH JSON events from stdin
  2. Parses event fields (hook_event_name, session_id, message, tool_name, etc.)
  3. Maps to HookEvent using existing memory-client types
  4. Converts to Event via map_hook_event()
  5. Sends to daemon via gRPC
  6. Returns {"continue":true} to stdout (always, even on failure)

Key Design Decisions

Decision Choice Rationale
Fail-open Always return success Never block Claude if memory is down
Type reuse Use memory-client types Avoid duplication, leverage tested code
Minimal binary ~200 lines Fast startup (<100ms), simple maintenance

Event Mapping

CCH Event HookEventType Memory EventType
SessionStart SessionStart session_start
UserPromptSubmit UserPromptSubmit user_message
AssistantResponse AssistantResponse assistant_message
PreToolUse ToolUse tool_result
PostToolUse ToolResult tool_result
Stop/SessionEnd Stop session_end
SubagentStart SubagentStart subagent_start
SubagentStop SubagentStop subagent_stop

Tests

11 unit tests covering:

  • JSON parsing for all event types
  • Event type mapping
  • Optional field handling (timestamp, tool_name, cwd)
  • End-to-end mapping pipeline

Files Changed

File Change
Cargo.toml Added memory-ingest to workspace members
crates/memory-ingest/Cargo.toml New crate manifest
crates/memory-ingest/src/main.rs CCH parsing and ingestion logic
examples/hooks.yaml Sample CCH configuration
docs/README.md Added CCH Integration section

Commits

Hash Message
38a26bd feat(08-01): implement CCH hook handler binary

Verification

# Build
cargo build --release -p memory-ingest
# OK - builds successfully

# Test with sample event (no daemon needed)
echo '{"hook_event_name":"UserPromptSubmit","session_id":"test-123","message":"Hello"}' | memory-ingest
# Output: {"continue":true}

# All unit tests pass
cargo test -p memory-ingest
# 11 passed

# Workspace tests pass
cargo test --workspace
# 141+ tests passed

Usage

Quick Setup

# Build
cargo build --release -p memory-ingest

# Install
cp target/aarch64-apple-darwin/release/memory-ingest ~/.local/bin/

# Configure CCH (Claude Code)
cp examples/hooks.yaml ~/.claude/hooks.yaml

# Start daemon
memory-daemon start

hooks.yaml Configuration

rules:
  - name: capture-to-memory
    matchers:
      operations:
        - SessionStart
        - UserPromptSubmit
        - PostToolUse
        - SessionEnd
    actions:
      run: "~/.local/bin/memory-ingest"

Deviations from Plan

None - plan executed exactly as written.

Next Phase Readiness

All tasks complete. Ready for:

  • Production deployment
  • Integration testing with live Claude Code sessions
  • Performance benchmarking under load

08-RESEARCH

Phase 8: CCH Hook Integration - Research

Researched: 2026-01-30 Domain: CCH hooks, stdin/stdout binary, event ingestion Confidence: HIGH

Summary

This phase creates a memory-ingest binary that integrates with Code-Agent Context Hooks (CCH) to automatically capture conversation events. The binary reads CCH events from stdin, maps them using existing memory-client code, and ingests them via gRPC.

Key insight: All the hard work is done. The memory-client crate already has:

  • HookEvent type matching CCH event types
  • map_hook_event() function for conversion
  • MemoryClient with ingest() for gRPC communication

The binary is just a thin stdin/stdout wrapper (~50 lines of Rust).

Standard Stack

Core

Library Version Purpose Why Standard
memory-client local HookEvent, map_hook_event, MemoryClient Already built in Phase 5
serde_json 1.0 Parse CCH JSON from stdin Standard JSON handling
tokio 1.43+ Async runtime for gRPC Workspace standard
clap 4.5+ CLI argument parsing Workspace standard

Supporting

Library Version Purpose When to Use
tracing 0.1 Debug logging Development
anyhow 1.0 Error handling Simplifies main()

Architecture

CCH (hooks.yaml)                    Agent Memory
┌─────────────────┐                 ┌─────────────────┐
│  SessionStart   │─────┐           │                 │
│  UserPrompt     │     │   stdin   │  memory-ingest  │──────► memory-daemon
│  PostToolUse    │─────┼──────────►│  (reads JSON,   │  gRPC  (gRPC :50051)
│  SessionEnd     │     │           │   maps, sends)  │
└─────────────────┘─────┘           └────────┬────────┘
                                             │
                                             ▼ stdout
                                    {"continue": true}

CCH Event Format

CCH sends JSON events on stdin (from CCH source code):

{
  "hook_event_name": "UserPromptSubmit",
  "session_id": "abc123",
  "tool_name": null,
  "tool_input": null,
  "timestamp": "2026-01-30T12:00:00Z",
  "cwd": "/path/to/project",
  "transcript_path": "/path/to/transcript.jsonl",
  "message": "Hello, how are you?"
}

Event types from CCH:

  • SessionStart - Session began
  • UserPromptSubmit - User sent a message
  • PreToolUse - Tool about to be used
  • PostToolUse - Tool completed
  • Stop - Session ended
  • SubagentStart - Subagent spawned
  • SubagentStop - Subagent completed

Implementation Pattern

// crates/memory-ingest/src/main.rs

use std::io::{self, BufRead, Write};
use memory_client::{MemoryClient, HookEvent, HookEventType, map_hook_event};
use serde::Deserialize;

#[derive(Deserialize)]
struct CchEvent {
    hook_event_name: String,
    session_id: String,
    message: Option<String>,
    tool_name: Option<String>,
    timestamp: Option<String>,
    // ... other fields
}

fn main() -> anyhow::Result<()> {
    // Read JSON from stdin
    let stdin = io::stdin();
    let mut input = String::new();
    stdin.lock().read_line(&mut input)?;

    // Parse CCH event
    let cch: CchEvent = serde_json::from_str(&input)?;

    // Map to HookEvent
    let hook_event = map_cch_to_hook(&cch);

    // Map to memory Event
    let event = map_hook_event(hook_event);

    // Ingest via gRPC (requires tokio runtime)
    let rt = tokio::runtime::Runtime::new()?;
    rt.block_on(async {
        match MemoryClient::connect_default().await {
            Ok(mut client) => {
                let _ = client.ingest(event).await;
            }
            Err(_) => {
                // Fail open - don't block Claude if daemon is down
            }
        }
    });

    // Return success JSON to CCH
    let response = serde_json::json!({ "continue": true });
    println!("{}", response);

    Ok(())
}

hooks.yaml Configuration

# .claude/hooks.yaml
version: "1.0"

settings:
  fail_open: true     # If memory-ingest fails, don't block Claude
  script_timeout: 5   # 5 second timeout

rules:
  - name: capture-to-memory
    description: Send all events to agent-memory daemon
    matchers:
      operations:
        - SessionStart
        - UserPromptSubmit
        - PostToolUse
        - SessionEnd
        - SubagentStart
        - SubagentStop
    actions:
      run: "~/.local/bin/memory-ingest"

Event Mapping

CCH Event HookEventType Memory EventType
SessionStart SessionStart SessionStart
UserPromptSubmit UserPromptSubmit UserMessage
PreToolUse ToolUse ToolResult
PostToolUse ToolResult ToolResult
Stop Stop SessionEnd
SubagentStart SubagentStart SubagentStart
SubagentStop SubagentStop SubagentStop

Don't Hand-Roll

Problem Don't Build Use Instead Why
Event parsing Custom struct Existing HookEvent Already tested
Event mapping New logic map_hook_event() Phase 5 work
gRPC client Raw HTTP MemoryClient Handles proto conversion

Common Pitfalls

Pitfall 1: Blocking on gRPC

What goes wrong: Using async incorrectly causes hangs

Solution: Use tokio::runtime::Runtime::new().block_on() pattern

Pitfall 2: Daemon Not Running

What goes wrong: Binary fails when daemon is down

Solution: Catch connection errors, fail open with {"continue": true}

Pitfall 3: CCH Field Names

What goes wrong: CCH uses hook_event_name not event_type

Solution: Use proper serde field aliases or custom struct

Verification

# Build binary
cargo build --release -p memory-ingest

# Test with sample event
echo '{"hook_event_name":"UserPromptSubmit","session_id":"test","message":"Hello"}' | ./target/release/memory-ingest

# Should output: {"continue":true}

# Install to local bin
cp target/release/memory-ingest ~/.local/bin/

# Test with CCH (manual)
claude # Run Claude Code with hooks.yaml configured

Sources

Primary (HIGH confidence)

  • crates/memory-client/src/hook_mapping.rs - HookEvent types and mapping
  • crates/memory-client/src/client.rs - MemoryClient API
  • CCH documentation (hooks.yaml format)

Secondary (MEDIUM confidence)

  • CCH source code (event format details)

Metadata

Confidence breakdown:

  • Event mapping: HIGH - Already built and tested in memory-client
  • Binary structure: HIGH - Standard stdin/stdout pattern
  • CCH format: MEDIUM - Based on documentation, verify with real CCH

Research date: 2026-01-30 Valid until: Stable (builds on existing code)


Clone this wiki locally