-
Notifications
You must be signed in to change notification settings - Fork 2
Phase 8 CCH Hook Integration
This page aggregates all Phase 8 documentation for the CCH Hook Integration phase.
Automatic event capture via CCH hooks.
Phase: 8 - CCH Hook Integration Wave: 1 Depends on: Phase 7 complete, memory-client crate
Create a memory-ingest binary that CCH can invoke to automatically capture conversation events and send them to memory-daemon.
The memory-client crate already provides:
-
HookEventtype with all CCH event types -
map_hook_event()function for conversion -
MemoryClientwithingest()for gRPC
This plan creates a thin CLI wrapper (~50 lines) that:
- Reads CCH JSON from stdin
- Converts to HookEvent
- Maps to memory Event
- Sends via gRPC
- Returns
{"continue": true}to stdout
What: Set up new binary crate in workspace
Files:
crates/memory-ingest/Cargo.tomlcrates/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
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
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
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
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-ingestworks -
cargo build --release -p memory-ingestproduces optimized binary
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
What: Unit tests for event mapping
Tests:
test_parse_session_starttest_parse_user_prompttest_parse_tool_usetest_map_all_event_typestest_fail_open_behavior
Acceptance Criteria:
- All tests pass
- Coverage for all event types
What: Add installation and usage instructions
Update: docs/README.md - Add CCH Integration section
Acceptance Criteria:
- Installation instructions
- hooks.yaml configuration example
- Troubleshooting section
# 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- Binary reads CCH JSON from stdin
- Binary outputs
{"continue":true}to stdout - Events are ingested when daemon is running
- Binary fails open when daemon is down (no hang, returns success)
- All CCH event types are mapped correctly
- Example hooks.yaml provided
- 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-clientcode - don't duplicate logic
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
One-liner: Lightweight memory-ingest binary for CCH hook integration with fail-open semantics
A minimal Rust binary (crates/memory-ingest) that integrates with code_agent_context_hooks (CCH):
- Reads CCH JSON events from stdin
- Parses event fields (hook_event_name, session_id, message, tool_name, etc.)
- Maps to HookEvent using existing memory-client types
-
Converts to Event via
map_hook_event() - Sends to daemon via gRPC
-
Returns
{"continue":true}to stdout (always, even on failure)
| 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 |
| 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 |
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
| 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 |
| Hash | Message |
|---|---|
| 38a26bd | feat(08-01): implement CCH hook handler binary |
# 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# 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 startrules:
- name: capture-to-memory
matchers:
operations:
- SessionStart
- UserPromptSubmit
- PostToolUse
- SessionEnd
actions:
run: "~/.local/bin/memory-ingest"None - plan executed exactly as written.
All tasks complete. Ready for:
- Production deployment
- Integration testing with live Claude Code sessions
- Performance benchmarking under load
Researched: 2026-01-30 Domain: CCH hooks, stdin/stdout binary, event ingestion Confidence: HIGH
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:
-
HookEventtype matching CCH event types -
map_hook_event()function for conversion -
MemoryClientwithingest()for gRPC communication
The binary is just a thin stdin/stdout wrapper (~50 lines of Rust).
| 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 |
| Library | Version | Purpose | When to Use |
|---|---|---|---|
| tracing | 0.1 | Debug logging | Development |
| anyhow | 1.0 | Error handling | Simplifies main() |
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 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
// 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(())
}# .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"| 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 |
| 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 |
What goes wrong: Using async incorrectly causes hangs
Solution: Use tokio::runtime::Runtime::new().block_on() pattern
What goes wrong: Binary fails when daemon is down
Solution: Catch connection errors, fail open with {"continue": true}
What goes wrong: CCH uses hook_event_name not event_type
Solution: Use proper serde field aliases or custom struct
# 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-
crates/memory-client/src/hook_mapping.rs- HookEvent types and mapping -
crates/memory-client/src/client.rs- MemoryClient API - CCH documentation (hooks.yaml format)
- CCH source code (event format details)
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)