diff --git a/CHANGELOG.md b/CHANGELOG.md index ad7f44a..61b3e90 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,19 @@ All notable changes to CLX will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/), and this project adheres to [Semantic Versioning](https://semver.org/). +## [0.7.1] - 2026-05-02 + +### Fixed +- Audit log foreign-key constraint failure on every L0-decided hook call. + `Storage::create_audit_log` now ensures the referenced session row exists + via `INSERT OR IGNORE` before the audit insert. Synthetic / fast-path / + fabricated session IDs no longer trip the FK. +- File logging was never wired up — `logging.file: ~/.clx/logs/clx.log` in + the config was silently ignored. `clx-hook` now opens the configured log + path (with `~` expansion already implemented in `Config::log_file_path`) + and writes WARN+ events there. stderr remains ERROR-only so Claude Code's + hook stderr-handling is unaffected. + ## [0.7.0] - 2026-04-30 ### Added diff --git a/Cargo.lock b/Cargo.lock index d1598de..278b57e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -549,7 +549,7 @@ checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32" [[package]] name = "clx" -version = "0.7.0" +version = "0.7.1" dependencies = [ "anyhow", "assert_cmd", @@ -579,7 +579,7 @@ dependencies = [ [[package]] name = "clx-core" -version = "0.7.0" +version = "0.7.1" dependencies = [ "anyhow", "chrono", @@ -608,7 +608,7 @@ dependencies = [ [[package]] name = "clx-hook" -version = "0.7.0" +version = "0.7.1" dependencies = [ "anyhow", "chrono", @@ -627,7 +627,7 @@ dependencies = [ [[package]] name = "clx-mcp" -version = "0.7.0" +version = "0.7.1" dependencies = [ "anyhow", "chrono", diff --git a/Cargo.toml b/Cargo.toml index 5355586..79fe3cb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ members = [ ] [workspace.package] -version = "0.7.0" +version = "0.7.1" edition = "2024" license = "MPL-2.0" authors = ["CLX Contributors"] diff --git a/crates/clx-core/src/storage/audit.rs b/crates/clx-core/src/storage/audit.rs index 1f30a17..904d1aa 100644 --- a/crates/clx-core/src/storage/audit.rs +++ b/crates/clx-core/src/storage/audit.rs @@ -11,8 +11,19 @@ use super::util::parse_datetime; use crate::types::{AuditDecision, AuditLogEntry, UserDecision}; impl Storage { - /// Create an audit log entry + /// Create an audit log entry. + /// + /// First ensures the referenced session row exists (INSERT OR IGNORE with + /// safe defaults). Without this guard, fast-path / synthetic / fabricated + /// session IDs trip the `audit_log` → `sessions` FOREIGN KEY constraint. pub fn create_audit_log(&self, entry: &AuditLogEntry) -> crate::Result { + // Ensure the FK target exists. No-op if the session was already created + // by SessionStart hook; a synthetic placeholder otherwise. + self.conn.execute( + "INSERT OR IGNORE INTO sessions (id, project_path, started_at, source, status) \ + VALUES (?1, '', datetime('now'), 'audit-placeholder', 'active')", + params![entry.session_id], + )?; self.conn.execute( "INSERT INTO audit_log (session_id, timestamp, command, working_dir, layer, decision, risk_score, reasoning, user_decision) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)", diff --git a/crates/clx-hook/src/main.rs b/crates/clx-hook/src/main.rs index 917dedc..f09258c 100644 --- a/crates/clx-hook/src/main.rs +++ b/crates/clx-hook/src/main.rs @@ -66,14 +66,43 @@ async fn main() -> Result<()> { clx_core::init_sqlite_vec(); - // Initialize tracing - only ERROR level to avoid confusing Claude Code - // Claude Code interprets any stderr output as hook errors - tracing_subscriber::fmt() - .with_env_filter( + // Initialize tracing. + // - stderr: ERROR only (Claude Code treats hook stderr as failure noise). + // - file: WARN+ to the configured log file (created by hook on first write). + // Ensures the user-visible "log file silently dropped" surprise is fixed. + let log_path = clx_core::config::Config::load().ok().and_then(|c| { + let p = c.log_file_path(); + std::fs::create_dir_all(p.parent()?).ok()?; + std::fs::OpenOptions::new() + .append(true) + .create(true) + .open(&p) + .ok() + .map(std::sync::Mutex::new) + .map(std::sync::Arc::new) + }); + let stderr_layer = tracing_subscriber::fmt::layer() + .with_writer(std::io::stderr) + .with_filter( tracing_subscriber::EnvFilter::from_default_env() .add_directive(tracing::Level::ERROR.into()), - ) - .with_writer(std::io::stderr) + ); + let file_layer = log_path.as_ref().map(|f| { + let f = f.clone(); + tracing_subscriber::fmt::layer() + .with_ansi(false) + .with_writer(move || MutexFile(f.clone())) + .with_filter( + tracing_subscriber::EnvFilter::try_from_default_env() + .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("warn")), + ) + }); + use tracing_subscriber::Layer; + use tracing_subscriber::layer::SubscriberExt; + use tracing_subscriber::util::SubscriberInitExt; + tracing_subscriber::registry() + .with(stderr_layer) + .with(file_layer) .init(); // Read JSON input from stdin (limited to 1MB to prevent DoS via memory exhaustion) @@ -140,3 +169,22 @@ async fn main() -> Result<()> { Ok(()) } + +/// Adapter so `Arc>` can serve as a tracing-subscriber writer. +/// Each write acquires the mutex; fine for a short-lived hook process. +struct MutexFile(std::sync::Arc>); + +impl std::io::Write for MutexFile { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + self.0 + .lock() + .map_err(|_| std::io::Error::other("file mutex poisoned"))? + .write(buf) + } + fn flush(&mut self) -> std::io::Result<()> { + self.0 + .lock() + .map_err(|_| std::io::Error::other("file mutex poisoned"))? + .flush() + } +}