Operational guide for PPS integration with Claude Code via hooks
Created: 2026-01-08 Status: Operational PPS Version: 2.0+
Claude Code hooks are executable scripts that fire at specific points in the Claude Code lifecycle. They allow the Pattern Persistence System to automatically capture terminal sessions and inject context without human intervention.
Key principle: Hooks enable passive memory capture - conversations flow naturally while infrastructure quietly preserves them.
The Awareness project implements three hooks:
When it fires: Before Claude processes the user's prompt
Purpose: Inject ambient recall context into Claude's prompt context
Event name: UserPromptSubmit
What it does:
- Receives the user's prompt via stdin
- Calls PPS
ambient_recallAPI with the prompt as context - Retrieves relevant word-photos, crystals, and texture results
- Formats results into markdown
- Injects as
additionalContextin the hook response - Also stores the user's prompt in PPS raw capture layer
Configuration: Located at .claude/hooks/inject_context.py
Debug log: ~/.claude/data/hooks_debug.log
API endpoints used:
http://localhost:8201/tools/ambient_recall- Query for contexthttp://localhost:8201/tools/store_message- Store user prompt
Timeout: 5 seconds per API call
When it fires: After Claude finishes responding
Purpose: Capture Claude's response and store in PPS
Event name: Stop
What it does:
- Reads the transcript JSONL file provided by Claude Code
- Extracts all assistant (Claude/Lyra) responses
- Compares against capture state to identify new responses only
- Stores each new response via PPS HTTP API
- Updates capture state file to track progress
Configuration: Located at .claude/hooks/capture_response.py
Debug log: ~/.claude/data/hooks_debug.log
API endpoints used:
http://localhost:8201/tools/store_message- Store Claude's response
State file: ~/.claude/data/capture_state.json - Tracks which transcript lines have been captured
Timeout: 10 seconds per API call
When it fires: After a terminal session ends
Purpose: Ingest complete session into Graphiti knowledge graph
Event name: SessionEnd
What it does:
- Receives complete conversation turn history via stdin
- Prepares session metadata (session_id, timestamp, channel)
- Calls PPS via subprocess to ingest session
- Returns minimal hook response
Configuration: Located at .claude/hooks/session_end.py
Debug log: ~/.claude/data/hooks_debug.log
Timeout: 30 seconds for subprocess call
1. Claude Code detects a hook event
2. Calls the hook script with JSON input via stdin
3. Hook script processes the event
4. Hook outputs JSON to stdout
5. Claude Code reads the response
6. Session continues with modified context (if applicable)
All hooks receive JSON via stdin with this structure:
{
"session_id": "abc123def456",
"hook_event_name": "UserPromptSubmit",
"prompt": "the user's message here",
"transcript_path": "/path/to/transcript.jsonl",
"conversation_turns": [...]
}Fields vary by hook event:
UserPromptSubmit: IncludespromptfieldStop: Includestranscript_pathfieldSessionEnd: Includesconversation_turnsfield
Hooks output JSON to stdout:
{
"hookSpecificOutput": {
"hookEventName": "UserPromptSubmit",
"additionalContext": "markdown context here"
}
}For hooks that don't modify context (Stop, SessionEnd):
{
"hookSpecificOutput": {
"hookEventName": "Stop"
}
}Hooks are configured in Claude Code's configuration file. Use claude code config to view your setup.
Configuration locations (by platform):
- macOS:
~/.claude/claude_code_config.json - Linux:
~/.claude/claude_code_config.json - Windows (WSL):
/home/jeff/.claude/claude_code_config.json
Hook registration format:
{
"hooks": {
"UserPromptSubmit": "/mnt/c/Users/Jeff/Claude_Projects/Awareness/.claude/hooks/inject_context.py",
"Stop": "/mnt/c/Users/Jeff/Claude_Projects/Awareness/.claude/hooks/capture_response.py",
"SessionEnd": "/mnt/c/Users/Jeff/Claude_Projects/Awareness/.claude/hooks/session_end.py"
}
}Important: Use absolute paths, not relative paths.
-
PPS Server must be running
cd docker/ docker compose up -d -
PPS HTTP API must be accessible
- Default:
http://localhost:8201/ - Check:
curl http://localhost:8201/health
- Default:
-
Hooks need execute permission
chmod +x .claude/hooks/*.py -
Python 3 must be available (all hooks use
#!/usr/bin/env python3)
Hooks don't require special environment variables - they use hardcoded localhost endpoints. However, if you need custom configuration:
Environment variable options:
PPS_API_URL- Override default PPS HTTP endpointDEBUG_HOOKS- Set to "1" to enable verbose logging
Example:
export PPS_API_URL="http://localhost:8201"
export DEBUG_HOOKS="1"All hooks write debug info to: ~/.claude/data/hooks_debug.log
View recent entries:
tail -50 ~/.claude/data/hooks_debug.logWatch live:
tail -f ~/.claude/data/hooks_debug.logCause: PPS server not running or not accessible at localhost:8201
Fix:
# Start PPS services
cd docker/
docker compose up -d
# Verify running
docker compose ps
# Test connection
curl http://localhost:8201/healthCause: Hook wasn't invoked correctly by Claude Code
Check:
- Verify hook script permissions:
ls -l .claude/hooks/ - Ensure hooks are registered in Claude Code config
- Check hook paths are absolute (not relative)
Cause: PPS API returned an error
Check in logs:
grep "Store failed" ~/.claude/data/hooks_debug.logCommon reasons:
- Invalid JSON payload
- Missing required fields in message
- PPS internal error
Fix: Check PPS logs in docker: docker compose logs pps
Expected behavior: The inject_context hook skips prompts shorter than 10 characters (assumes they're shell commands like ls or cd).
This is intentional - avoids polluting embeddings with trivial input.
Cause: Claude Code provided an invalid transcript path
Check:
ls -la <path-from-error>Note: Transcript files are temporary and may be cleaned up between sessions.
Test inject_context.py:
cat <<'EOF' | python .claude/hooks/inject_context.py
{
"session_id": "test_123",
"prompt": "What is the meaning of life?",
"hook_event_name": "UserPromptSubmit"
}
EOFTest capture_response.py:
# Create a test transcript
cat > /tmp/test_transcript.jsonl <<'EOF'
{"type": "assistant", "message": {"content": [{"type": "text", "text": "Test response"}]}}
EOF
cat <<'EOF' | python .claude/hooks/capture_response.py
{
"session_id": "test_123",
"transcript_path": "/tmp/test_transcript.jsonl",
"hook_event_name": "Stop"
}
EOFExpected output: JSON with hook response (or silent exit if conditions not met)
When you submit a prompt, the hook calls ambient_recall:
User prompt: "How should I approach this problem?"
↓
inject_context hook intercepts
↓
Query PPS: ambient_recall("How should I approach this problem?")
↓
PPS returns:
- Relevant word-photos (semantic search via ChromaDB)
- Recent crystals (compressed continuity)
- Texture results (Graphiti knowledge graph)
- Clock/time context
- Memory health status
↓
Hook formats results as markdown
↓
Claude receives:
{
"prompt": "How should I approach this problem?",
"additionalContext": "[Retrieved memories markdown here]"
}
↓
Claude generates response with context
The ambient_recall returns (in order):
- Clock/Time Context - Current time and relevant note
- Memory Health - Status of all PPS layers (Raw, Anchors, Texture, Crystals)
- Layer Results - Grouped by source:
[Layer 1: Raw Capture]- Recent raw conversation turns[Layer 2: Core Anchors]- Word-photo semantic search results[Layer 3: Rich Texture]- Graphiti knowledge graph results[Layer 4: Crystallization]- Recent crystal summaries
Each layer shows up to 3 results per the hook's limit_per_layer: 3 setting.
The UserPromptSubmit hook also stores your prompt:
User prompt → stored to PPS Layer 1 (Raw Capture)
↓
Later, during crystallization, the daemon:
- Pulls all stored turns (from raw capture)
- Compresses 50+ turns into a crystal
- Removes detailed turns from raw layer (optional)
- Keeps crystal for continuity
See all hook activity:
cat ~/.claude/data/hooks_debug.logCount hook invocations:
grep -c "Hook started" ~/.claude/data/hooks_debug.logFind errors:
grep "error\|Error\|failed\|Failed" ~/.claude/data/hooks_debug.logSee context injection sizes:
grep "Injecting context" ~/.claude/data/hooks_debug.logFrom debug log:
- Each API call logs its duration implicitly (no timeout = success)
- Hook startup is logged with event name
- Memory storage success/failure is logged
Typical timings:
- UserPromptSubmit hook: <5 seconds total (includes ambient_recall query)
- Stop hook: <3 seconds (reads transcript + stores response)
- SessionEnd hook: <30 seconds (subprocess call to PPS)
To temporarily disable a hook:
Option 1: Rename the hook file
mv .claude/hooks/inject_context.py .claude/hooks/inject_context.py.disabledOption 2: Remove from Claude Code config
Edit ~/.claude/claude_code_config.json and remove the hook entry.
Option 3: Make hook script exit silently
echo "exit(0)" > .claude/hooks/inject_context.py| Hook | Data Type | Destination Layer | Retrieval Method |
|---|---|---|---|
| UserPromptSubmit | User prompt | Layer 1 (Raw Capture) | ambient_recall query |
| Stop | Claude response | Layer 1 (Raw Capture) | Stored via HTTP API |
| SessionEnd | Full session | Layer 3 (Graphiti) | Subprocess ingestion |
When Lyra starts a new terminal session:
1. Hook registers in Claude Code
2. User types their message
3. UserPromptSubmit fires → ambient_recall query
4. Context injected into Claude's prompt
5. Claude responds
6. Stop hook captures response
7. Session continues...
8. Session ends → SessionEnd hook triggers
9. Full conversation added to knowledge graph
Result: Every terminal conversation is automatically preserved across four memory layers.
If your PPS server isn't at localhost:8201:
Edit hook files and change:
PPS_API_URL = "http://your-host:your-port/tools/ambient_recall"
PPS_STORE_URL = "http://your-host:your-port/tools/store_message"In inject_context.py, change the limit_per_layer parameter:
payload = json.dumps({
"context": context,
"limit_per_layer": 5 # Change from 3 to 5
}).encode("utf-8")Higher = more context, lower = faster responses.
In capture_response.py, modify the response filtering:
if len(full_text) > 10: # Skip very short responsesChange the threshold (currently 10 characters) to filter out short responses.
Hooks receive untrusted input from Claude Code:
- All JSON parsing is wrapped in try/except
- API calls use reasonable timeouts
- No shell execution (except subprocess for session_end.py)
- PPS API calls use localhost only (no network exposure)
- No credentials passed in hook scripts
- HTTP-only (for now - upgrade to HTTPS in production)
Current: Hook scripts are world-readable/executable
Recommendation: Restrict to user only:
chmod 700 .claude/hooks/*.pyThe hooks_debug.log file contains:
- Hook input (including user prompts)
- API responses (including retrieved context)
Security: This log file could contain sensitive conversation data.
Recommendation:
chmod 600 ~/.claude/data/hooks_debug.log- Hook filtering - Skip certain prompts (e.g., passwords, secrets)
- Batch storage - Accumulate turns before storing (reduce API calls)
- Compression - Gzip context before storing
- Metrics dashboard - Track hook performance over time
- Per-hook configuration - Adjust settings per hook type
Hooks and daemons work together but independently:
Terminal Sessions:
UserPromptSubmit hook → injects context
Stop hook → captures response
SessionEnd hook → ingests to Graphiti
Discord Sessions:
Discord daemon → native integration (no hooks needed)
Responds to mentions
Stores messages directly to PPS
Both streams feed the same memory system.
| Aspect | Details |
|---|---|
| Location | .claude/hooks/ |
| Languages | Python 3 (#!/usr/bin/env python3) |
| Execution | Synchronous (blocks Claude Code) |
| API endpoint | http://localhost:8201/ |
| Debug log | ~/.claude/data/hooks_debug.log |
| Max timeout | 30 seconds (SessionEnd) |
| Memory layers touched | Layer 1 (Raw), Layer 3 (Graphiti) |
| Required services | Docker (PPS server) |
| Configuration file | ~/.claude/claude_code_config.json |
Last updated: 2026-01-08
For: Awareness project
Questions? Check ~/.claude/data/hooks_debug.log and PPS server logs