Two-layer observability — Claude Code hooks for dev-time, Rust tracing for runtime
Split observability into two layers with different purposes: Claude Code hooks capture every Bash/Edit/Prompt/Stop during dev, Rust `tracing` captures PTY lifecycle on the user's machine. Codified as CLAUDE.md RULE #8.
Why two layers
- Layer 1 (dev-time): every Bash, Edit/Write, UserPromptSubmit, and Stop event during a Claude Code session, auto-logged to
logs/{actions,edits,prompts,sessions}.jsonlvia.claude/settings.jsonhooks. Git-ignored. Raw material fordocs/ISSUES.mdpost-mortems and the prompt-quality blame section. - Layer 2 (runtime): every PTY lifecycle event (spawn, kill, EOF, read error, emit failure) and Tauri command invocation, written by Rust
tracing+tracing-appenderto~/Library/Logs/DalkkakAI/runtime.log.YYYY-MM-DD(daily rolling).
The two layers answer different questions. Layer 1 answers "why did Claude make that decision?". Layer 2 answers "what actually happened on the user's machine?". Conflating them produces a log that's bad at both.
What landed
Rust init at app start (apps/desktop/src-tauri/src/lib.rs):
fn init_logging() {
let log_dir = std::env::var("HOME")
.map(|h| format!("{}/Library/Logs/DalkkakAI", h))
.unwrap_or_else(|_| "./logs".to_string());
let _ = std::fs::create_dir_all(&log_dir);
let file_appender = tracing_appender::rolling::daily(&log_dir, "runtime.log");
let (non_blocking, guard) = tracing_appender::non_blocking(file_appender);
Box::leak(Box::new(guard)); // worker thread lives as long as the app
let filter = tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new(
"info,appsdesktop=debug,appsdesktop_lib=debug"
));
let _ = tracing_subscriber::fmt()
.with_writer(non_blocking)
.with_ansi(false)
.with_target(true)
.with_env_filter(filter)
.try_init();
}Commit 89a8c26. Rule codified as CLAUDE.md RULE #8.
Pattern
When you keep wanting to grep one log for two unrelated questions, that log is hiding a missing split. Build the two logs deliberately — separate filenames, separate writers, separate retention — and the question you're asking next will pick the right one.