Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 4 additions & 4 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ members = [
]

[workspace.package]
version = "0.7.0"
version = "0.7.1"
edition = "2024"
license = "MPL-2.0"
authors = ["CLX Contributors"]
Expand Down
13 changes: 12 additions & 1 deletion crates/clx-core/src/storage/audit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<i64> {
// 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)",
Expand Down
60 changes: 54 additions & 6 deletions crates/clx-hook/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -140,3 +169,22 @@ async fn main() -> Result<()> {

Ok(())
}

/// Adapter so `Arc<Mutex<File>>` can serve as a tracing-subscriber writer.
/// Each write acquires the mutex; fine for a short-lived hook process.
struct MutexFile(std::sync::Arc<std::sync::Mutex<std::fs::File>>);

impl std::io::Write for MutexFile {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
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()
}
}
Loading