use std::{path::Path, time::SystemTime};
use super::{
CAPTURE_ENV, CapturePaths, Harness, Invocation, SpawnPlan, detect_shape, is_uuid, last_hint,
pin_plan, resume_shape, shell_quote, unique_in_window,
};
pub struct Claude;
impl Harness for Claude {
fn home_env_var(&self) -> &'static str {
"CLAUDE_CONFIG_DIR"
}
fn home_dot_dir(&self) -> &'static str {
".claude"
}
fn detect(&self, cmd: &str) -> Option<Invocation> {
detect_shape(cmd, "claude", "--resume")
}
fn instrument(
&self,
inv: &Invocation,
capture: &CapturePaths,
_home: Option<&Path>,
) -> SpawnPlan {
let mut plan = pin_plan(inv);
plan.args_suffix.push_str(" --settings ");
plan.args_suffix
.push_str(&shell_quote(&capture.claude_settings.to_string_lossy()));
plan.env = vec![(
CAPTURE_ENV.into(),
capture.capture_file.clone().into_os_string(),
)];
plan
}
fn parse_capture(&self, payload: &str) -> Option<String> {
let v = jzon::parse(payload).ok()?;
let id = v["session_id"].as_str()?;
is_uuid(id).then(|| id.to_string())
}
fn scrape_exit(&self, text: &str) -> Option<String> {
last_hint(text, &["claude --resume "])
}
fn correlate_fs(&self, cwd: &Path, spawned: SystemTime, home: Option<&Path>) -> Option<String> {
let dir = self.home_root(home)?.join("projects").join(slug(cwd)?);
unique_in_window(dir, spawned, |entry| {
let path = entry.path();
if path.extension().and_then(|e| e.to_str()) != Some("jsonl") {
return None;
}
Some(path.file_stem()?.to_str()?.to_string())
})
}
fn resume_command(&self, cmd: &str, id: &str) -> String {
resume_shape(cmd, "claude", "--resume", id)
}
}
fn slug(cwd: &Path) -> Option<String> {
Some(
cwd.to_str()?
.chars()
.map(|c| if c == '/' || c == '.' { '-' } else { c })
.collect(),
)
}
#[cfg(test)]
mod tests {
use std::{fs, path::PathBuf};
use super::super::testutil::{ID, OTHER, paths};
use super::*;
use crate::testutil::{corpus_emulator, temp};
#[test]
fn everything_else_is_opaque_and_never_rewritten() {
let opaque: Vec<String> = [
"claude --model opus",
"claude --continue",
"claude -c",
"claude mcp list",
"claudius",
]
.iter()
.map(|s| s.to_string())
.chain([
format!("claude -r {ID}"),
format!("claude --resume {ID} --model opus"),
format!("claude --session-id {ID}"),
])
.collect();
for cmd in opaque {
assert_eq!(Claude.detect(&cmd), None, "{cmd:?} must be opaque");
assert_eq!(
Claude.resume_command(&cmd, ID),
cmd,
"an opaque command must never be rewritten"
);
}
}
#[test]
fn instrument_pins_an_id_and_layers_settings_on_bare_launches() {
let inv = Claude.detect("claude").unwrap();
let plan = Claude.instrument(&inv, &paths(), None);
let id = plan.injected_id.expect("a bare launch pins an id");
assert!(is_uuid(&id));
assert_eq!(
plan.args_suffix,
format!(" --session-id '{id}' --settings '/tmp/Application Support/fleetcom.json'")
);
assert_eq!(
plan.env,
vec![(
CAPTURE_ENV.into(),
PathBuf::from("/tmp/cap/session.json").into_os_string()
)]
);
}
#[test]
fn instrument_adds_only_settings_to_the_resume_form() {
let inv = Claude.detect(&format!("claude --resume {ID}")).unwrap();
let plan = Claude.instrument(&inv, &paths(), None);
assert_eq!(plan.injected_id, None);
assert_eq!(
plan.args_suffix,
" --settings '/tmp/Application Support/fleetcom.json'"
);
assert_eq!(plan.env.len(), 1, "env still names the capture file");
}
#[test]
fn parse_capture_returns_only_strict_ids() {
let payload = format!(
r#"{{"session_id":"{ID}","transcript_path":"/t/x.jsonl","cwd":"/w","hook_event_name":"SessionStart","source":"startup"}}"#
);
assert_eq!(Claude.parse_capture(&payload).as_deref(), Some(ID));
assert_eq!(Claude.parse_capture(r#"{"session_id":"NOT-VALID"}"#), None);
assert_eq!(
Claude.parse_capture(r#"{"session_id":"x'; rm -rf ~'"}"#),
None
);
assert_eq!(Claude.parse_capture("not json"), None);
assert_eq!(Claude.parse_capture("{}"), None);
}
#[test]
fn scrape_exit_takes_the_last_hint() {
let text = format!(
"Resume this session with:\nclaude --resume {OTHER}\n...\n\
Resume this session with:\nclaude --resume {ID}\n"
);
assert_eq!(Claude.scrape_exit(&text).as_deref(), Some(ID));
assert_eq!(Claude.scrape_exit("no hint here"), None);
assert_eq!(Claude.scrape_exit("claude --resume NOT-A-UUID"), None);
assert_eq!(Claude.scrape_exit(&format!("claude --resume {ID}ff")), None);
}
#[test]
fn correlate_fs_requires_a_unique_in_window_transcript() {
let home = temp("claude_correlate");
let cwd = Path::new("/a/b.c");
let dir = home.join("projects").join("-a-b-c");
fs::create_dir_all(&dir).unwrap();
fs::write(dir.join(format!("{ID}.jsonl")), "{}").unwrap();
let now = SystemTime::now();
assert_eq!(
Claude.correlate_fs(cwd, now, Some(&home)).as_deref(),
Some(ID)
);
let late = now + std::time::Duration::from_secs(120);
assert_eq!(Claude.correlate_fs(cwd, late, Some(&home)), None);
assert_eq!(
Claude.correlate_fs(Path::new("/other"), now, Some(&home)),
None
);
fs::write(dir.join(format!("{OTHER}.jsonl")), "{}").unwrap();
assert_eq!(Claude.correlate_fs(cwd, now, Some(&home)), None);
let _ = fs::remove_dir_all(&home);
}
#[test]
fn correlate_fs_rejects_a_unique_non_uuid_stem() {
let home = temp("claude_nonuuid");
let cwd = Path::new("/w");
let dir = home.join("projects").join("-w");
fs::create_dir_all(&dir).unwrap();
fs::write(dir.join("agent-notes.jsonl"), "{}").unwrap();
assert_eq!(
Claude.correlate_fs(cwd, SystemTime::now(), Some(&home)),
None
);
let _ = fs::remove_dir_all(&home);
}
#[test]
fn corpus_scrape_recovers_the_exit_hint_id() {
let mut emu = corpus_emulator();
emu.process(include_bytes!("../../tests/corpus/claude_resume.bin"));
assert_eq!(
Claude.scrape_exit(&emu.text_with_history()).as_deref(),
Some("c8c4a5cc-0b32-4ba0-a6b4-6ed08c218e0d")
);
}
}