use std::io::Read;
use std::process::ExitCode;
use crate::cmd::session::AgentKind;
use super::compound::{split_compound, try_rewrite_compound};
use super::engine::try_rewrite;
use super::types::CompoundSplitResult;
pub(super) fn parse_agent_flag(args: &[String]) -> Option<AgentKind> {
let mut i = 0;
while i < args.len() {
if args[i] == "--agent" {
i += 1;
if i < args.len() {
let result = AgentKind::from_str(&args[i]);
if result.is_none() {
crate::cmd::hook_log::log_hook_warning(&format!(
"unknown --agent value '{}', falling back to claude-code",
&args[i]
));
}
return result;
}
}
i += 1;
}
None
}
pub(super) const HOOK_MAX_STDIN_BYTES: u64 = 64 * 1024;
pub(super) const HOOK_TIMEOUT_SECS: u64 = 5;
pub(super) fn run_hook_mode(agent: Option<AgentKind>) -> anyhow::Result<ExitCode> {
use crate::cmd::hooks::{protocol_for_agent, HookSupport};
std::thread::spawn(|| {
std::thread::sleep(std::time::Duration::from_secs(HOOK_TIMEOUT_SECS));
crate::cmd::hook_log::log_hook_warning("hook processing timed out after 5s, exiting");
std::process::exit(0);
});
let agent_kind = agent.unwrap_or(AgentKind::ClaudeCode);
let protocol = protocol_for_agent(agent_kind);
if protocol.hook_support() == HookSupport::AwarenessOnly {
return Ok(ExitCode::SUCCESS);
}
if agent_kind == AgentKind::ClaudeCode {
let integrity_failed = check_hook_integrity(agent_kind);
if !integrity_failed {
check_hook_version_mismatch(agent_kind);
}
}
let mut stdin_buf = String::new();
let bytes_read = std::io::stdin()
.lock()
.take(HOOK_MAX_STDIN_BYTES)
.read_to_string(&mut stdin_buf);
let stdin_buf = match bytes_read {
Ok(_) => stdin_buf,
Err(_) => return Ok(ExitCode::SUCCESS), };
let json: serde_json::Value = match serde_json::from_str(&stdin_buf) {
Ok(v) => v,
Err(_) => {
audit_hook("", false, "");
return Ok(ExitCode::SUCCESS); }
};
let command = match protocol.parse_input(&json) {
Some(input) => input.command,
None => {
audit_hook("", false, "");
return Ok(ExitCode::SUCCESS); }
};
if command.starts_with("skim ") {
audit_hook(&command, false, "");
return Ok(ExitCode::SUCCESS);
}
let has_operator_chars = command.contains("&&")
|| command.contains("||")
|| command.contains(';')
|| command.contains('|');
let tokens: Vec<&str> = command.split_whitespace().collect();
if tokens.is_empty() {
audit_hook(&command, false, "");
return Ok(ExitCode::SUCCESS);
}
let original = tokens.join(" ");
let rewritten = if !has_operator_chars {
try_rewrite(&tokens).map(|r| r.tokens.join(" "))
} else {
match split_compound(&original) {
CompoundSplitResult::Bail => None,
CompoundSplitResult::Simple(simple_tokens) => {
let token_refs: Vec<&str> = simple_tokens.iter().map(|s| s.as_str()).collect();
try_rewrite(&token_refs).map(|r| r.tokens.join(" "))
}
CompoundSplitResult::Compound(segments) => {
try_rewrite_compound(&segments).map(|r| r.tokens.join(" "))
}
}
};
match rewritten {
Some(ref rewritten_cmd) => {
audit_hook(&command, true, rewritten_cmd);
let response = protocol.format_response(rewritten_cmd);
let json_out = serde_json::to_string(&response)?;
println!("{json_out}");
}
None => {
audit_hook(&command, false, "");
}
}
Ok(ExitCode::SUCCESS)
}
fn resolve_hook_config_dir(agent: AgentKind) -> Option<std::path::PathBuf> {
crate::cmd::init::resolve_config_dir_for_agent(false, agent).ok()
}
pub(super) fn should_warn_today(stamp_path: &std::path::Path) -> bool {
let today = today_date_string();
if let Ok(contents) = std::fs::read_to_string(stamp_path) {
if contents.trim() == today {
return false;
}
}
let _ = std::fs::create_dir_all(stamp_path.parent().unwrap_or(std::path::Path::new(".")));
let _ = std::fs::write(stamp_path, &today);
true
}
fn check_hook_integrity(agent: AgentKind) -> bool {
let config_dir = match resolve_hook_config_dir(agent) {
Some(dir) => dir,
None => return false,
};
let agent_name = agent.cli_name();
let script_path = config_dir.join("hooks").join("skim-rewrite.sh");
if !script_path.exists() {
return false;
}
match crate::cmd::integrity::verify_script_integrity(&config_dir, agent_name, &script_path) {
Ok(true) => false, Ok(false) => {
let stamp_path = match cache_dir() {
Some(dir) => dir.join(format!(".hook-integrity-warned-{agent_name}")),
None => {
crate::cmd::hook_log::log_hook_warning(&format!(
"hook script tampered: {}",
script_path.display()
));
return true;
}
};
if should_warn_today(&stamp_path) {
crate::cmd::hook_log::log_hook_warning(&format!(
"hook script tampered: {} (run `skim init --yes` to reinstall)",
script_path.display()
));
}
true
}
Err(_) => false, }
}
fn check_hook_version_mismatch(agent: AgentKind) {
let hook_version = match std::env::var("SKIM_HOOK_VERSION") {
Ok(v) => v,
Err(_) => return, };
let compiled_version = env!("CARGO_PKG_VERSION");
if hook_version == compiled_version {
return; }
let agent_name = agent.cli_name();
let stamp_path = match cache_dir() {
Some(dir) => dir.join(format!(".hook-version-warned-{agent_name}")),
None => return,
};
if should_warn_today(&stamp_path) {
crate::cmd::hook_log::log_hook_warning(&format!(
"version mismatch: hook script v{hook_version}, binary v{compiled_version} (run `skim init --yes` to update)"
));
}
}
const AUDIT_LOG_MAX_BYTES: u64 = 10 * 1024 * 1024;
const AUDIT_LOG_MAX_ARCHIVES: u32 = 3;
fn audit_hook(original: &str, matched: bool, rewritten: &str) {
if std::env::var("SKIM_HOOK_AUDIT").as_deref() != Ok("1") {
return;
}
let log_path = match cache_dir() {
Some(dir) => dir.join("hook-audit.log"),
None => return,
};
if let Ok(meta) = std::fs::metadata(&log_path) {
if meta.len() >= AUDIT_LOG_MAX_BYTES {
for i in (1..AUDIT_LOG_MAX_ARCHIVES).rev() {
let from = audit_archive_path(&log_path, i);
let to = audit_archive_path(&log_path, i + 1);
let _ = std::fs::rename(&from, &to);
}
let archive_1 = audit_archive_path(&log_path, 1);
let _ = std::fs::rename(&log_path, &archive_1);
}
}
let entry = serde_json::json!({
"timestamp": today_date_string(),
"original": original,
"matched": matched,
"rewritten": rewritten,
});
let _ = std::fs::create_dir_all(log_path.parent().unwrap_or(std::path::Path::new(".")));
if let Ok(mut file) = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(&log_path)
{
use std::io::Write;
let _ = writeln!(file, "{}", entry);
}
}
fn audit_archive_path(log_path: &std::path::Path, index: u32) -> std::path::PathBuf {
let mut path = log_path.as_os_str().to_owned();
path.push(format!(".{index}"));
std::path::PathBuf::from(path)
}
fn cache_dir() -> Option<std::path::PathBuf> {
crate::cmd::hook_log::cache_dir()
}
fn today_date_string() -> String {
let now = std::time::SystemTime::now();
let secs = now
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
let days = secs / 86400;
let (year, month, day) = crate::cmd::hook_log::days_to_date(days);
format!("{year:04}-{month:02}-{day:02}")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_agent_flag_present() {
let args = vec![
"--hook".to_string(),
"--agent".to_string(),
"claude-code".to_string(),
];
assert_eq!(parse_agent_flag(&args), Some(AgentKind::ClaudeCode));
}
#[test]
fn test_parse_agent_flag_codex() {
let args = vec![
"--hook".to_string(),
"--agent".to_string(),
"codex".to_string(),
];
assert_eq!(parse_agent_flag(&args), Some(AgentKind::CodexCli));
}
#[test]
fn test_parse_agent_flag_absent() {
let args = vec!["--hook".to_string()];
assert_eq!(parse_agent_flag(&args), None);
}
#[test]
fn test_parse_agent_flag_missing_value() {
let args = vec!["--hook".to_string(), "--agent".to_string()];
assert_eq!(parse_agent_flag(&args), None);
}
#[test]
fn test_hook_timeout_constant() {
assert_eq!(
HOOK_TIMEOUT_SECS, 5,
"Hook timeout must be 5 seconds (Claude Code hook timeout is 5s)"
);
}
#[test]
fn test_hook_max_stdin_bytes_constant() {
assert_eq!(
HOOK_MAX_STDIN_BYTES,
64 * 1024,
"Hook max stdin must be 64 KiB"
);
}
#[test]
fn test_should_warn_today_no_stamp() {
let dir = tempfile::TempDir::new().unwrap();
let stamp = dir.path().join("stamp");
assert!(
should_warn_today(&stamp),
"should warn when no stamp exists"
);
assert!(stamp.exists(), "stamp file should be created");
}
#[test]
fn test_should_warn_today_same_day_stamp() {
let dir = tempfile::TempDir::new().unwrap();
let stamp = dir.path().join("stamp");
assert!(should_warn_today(&stamp), "first call should warn");
assert!(
!should_warn_today(&stamp),
"second call same day should not warn"
);
}
}