From b5cff1b8fc3d53ccd51ab0a42b81aeaccd7a450b Mon Sep 17 00:00:00 2001 From: tupe12334 Date: Thu, 11 Jun 2026 21:37:12 +0300 Subject: [PATCH 1/4] feat: clean up session dir on session:stop event Co-Authored-By: Claude Sonnet 4.6 --- core/src/run.rs | 126 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 126 insertions(+) diff --git a/core/src/run.rs b/core/src/run.rs index 06d21eb..8168941 100644 --- a/core/src/run.rs +++ b/core/src/run.rs @@ -16,6 +16,33 @@ use crate::state::{init_state, load_state, save_state, HookEvent, HookResponse, /// `HookResponse::Block { message }` with the gate message. pub fn run(event: &HookEvent, repo_root: &Path) -> Result { let steplock_dir = repo_root.join(".steplock"); + + // session:stop — clean up the scope dir and approve. + if event.event == "session:stop" { + if steplock_dir.exists() { + let scope_key = if !event.session_id.is_empty() { + Some(event.session_id.clone()) + } else { + let fallback_path = steplock_dir.join("sessions").join("fallback-id"); + if fallback_path.exists() { + fs::read_to_string(&fallback_path) + .ok() + .map(|s| s.trim().to_owned()) + } else { + None + } + }; + if let Some(key) = scope_key { + let scope_dir = steplock_dir.join("sessions").join(&key); + if scope_dir.exists() { + fs::remove_dir_all(&scope_dir)?; + eprintln!("steplock: cleaned up session {}", key); + } + } + } + return Ok(HookResponse::Approve); + } + let checklists_dir = steplock_dir.join("checklists"); if !checklists_dir.exists() { @@ -582,6 +609,105 @@ reset = "always" } } + #[test] + fn session_stop_removes_scope_dir() { + let tmp = TempDir::new().unwrap(); + setup_checklist(tmp.path()); + + // First block creates the session dir + let event = make_event("tool:before", "bash", "git push origin main", "sess-stop"); + let _ = run(&event, tmp.path()).unwrap(); + let scope_dir = tmp.path().join(".steplock/sessions/sess-stop"); + assert!(scope_dir.exists()); + + // session:stop removes the scope dir + let stop = HookEvent { + event: "session:stop".to_owned(), + tool: String::new(), + input: HashMap::new(), + output: HashMap::new(), + session_id: "sess-stop".to_owned(), + caller: "claude-code".to_owned(), + }; + let resp = run(&stop, tmp.path()).unwrap(); + assert!(matches!(resp, HookResponse::Approve)); + assert!(!scope_dir.exists()); + } + + #[test] + fn session_stop_approves_when_no_scope_dir() { + let tmp = TempDir::new().unwrap(); + setup_checklist(tmp.path()); + let stop = HookEvent { + event: "session:stop".to_owned(), + tool: String::new(), + input: HashMap::new(), + output: HashMap::new(), + session_id: "nonexistent-session".to_owned(), + caller: "claude-code".to_owned(), + }; + let resp = run(&stop, tmp.path()).unwrap(); + assert!(matches!(resp, HookResponse::Approve)); + } + + #[test] + fn session_stop_approves_when_no_steplock_dir() { + let tmp = TempDir::new().unwrap(); + let stop = HookEvent { + event: "session:stop".to_owned(), + tool: String::new(), + input: HashMap::new(), + output: HashMap::new(), + session_id: "sess-x".to_owned(), + caller: "claude-code".to_owned(), + }; + let resp = run(&stop, tmp.path()).unwrap(); + assert!(matches!(resp, HookResponse::Approve)); + } + + #[test] + fn session_stop_uses_fallback_id_when_session_id_empty() { + let tmp = TempDir::new().unwrap(); + setup_checklist(tmp.path()); + + // First block with empty session_id creates fallback-id and a scope dir + let mut input = HashMap::new(); + input.insert( + "command".to_owned(), + serde_json::Value::String("git push".to_owned()), + ); + let no_session = HookEvent { + event: "tool:before".to_owned(), + tool: "bash".to_owned(), + input, + output: HashMap::new(), + session_id: "".to_owned(), + caller: "unknown".to_owned(), + }; + let _ = run(&no_session, tmp.path()).unwrap(); + + let fallback_id = fs::read_to_string( + tmp.path().join(".steplock/sessions/fallback-id"), + ) + .unwrap(); + let fallback_id = fallback_id.trim(); + let scope_dir = tmp.path().join(".steplock/sessions").join(fallback_id); + assert!(scope_dir.exists()); + + // session:stop with empty session_id uses fallback-id to clean up + let stop = HookEvent { + event: "session:stop".to_owned(), + tool: String::new(), + input: HashMap::new(), + output: HashMap::new(), + session_id: "".to_owned(), + caller: "unknown".to_owned(), + }; + let resp = run(&stop, tmp.path()).unwrap(); + assert!(matches!(resp, HookResponse::Approve)); + assert!(!scope_dir.exists()); + } + #[test] fn unknown_current_state_in_flow_skips_checklist() { let tmp = TempDir::new().unwrap(); From e4e053a3d32caf98e514714829dd9ff1c5c14f9c Mon Sep 17 00:00:00 2001 From: tupe12334 Date: Thu, 11 Jun 2026 23:06:18 +0300 Subject: [PATCH 2/4] refactor session:stop cleanup into helper with early returns --- core/src/run.rs | 46 ++++++++++++++++++++++++---------------------- 1 file changed, 24 insertions(+), 22 deletions(-) diff --git a/core/src/run.rs b/core/src/run.rs index 8168941..f933560 100644 --- a/core/src/run.rs +++ b/core/src/run.rs @@ -17,29 +17,8 @@ use crate::state::{init_state, load_state, save_state, HookEvent, HookResponse, pub fn run(event: &HookEvent, repo_root: &Path) -> Result { let steplock_dir = repo_root.join(".steplock"); - // session:stop — clean up the scope dir and approve. if event.event == "session:stop" { - if steplock_dir.exists() { - let scope_key = if !event.session_id.is_empty() { - Some(event.session_id.clone()) - } else { - let fallback_path = steplock_dir.join("sessions").join("fallback-id"); - if fallback_path.exists() { - fs::read_to_string(&fallback_path) - .ok() - .map(|s| s.trim().to_owned()) - } else { - None - } - }; - if let Some(key) = scope_key { - let scope_dir = steplock_dir.join("sessions").join(&key); - if scope_dir.exists() { - fs::remove_dir_all(&scope_dir)?; - eprintln!("steplock: cleaned up session {}", key); - } - } - } + cleanup_session(&steplock_dir, &event.session_id)?; return Ok(HookResponse::Approve); } @@ -201,6 +180,29 @@ pub fn run(event: &HookEvent, repo_root: &Path) -> Result { Ok(HookResponse::Approve) } +fn cleanup_session(steplock_dir: &Path, session_id: &str) -> Result<()> { + if !steplock_dir.exists() { + return Ok(()); + } + let scope_key = if !session_id.is_empty() { + session_id.to_owned() + } else { + let fallback_path = steplock_dir.join("sessions").join("fallback-id"); + if !fallback_path.exists() { + return Ok(()); + } + fs::read_to_string(&fallback_path) + .map(|s| s.trim().to_owned())? + }; + let scope_dir = steplock_dir.join("sessions").join(&scope_key); + if !scope_dir.exists() { + return Ok(()); + } + fs::remove_dir_all(&scope_dir)?; + eprintln!("steplock: cleaned up session {}", scope_key); + Ok(()) +} + fn get_scope_key(event: &HookEvent, steplock_dir: &Path) -> Result { if !event.session_id.is_empty() { return Ok(event.session_id.clone()); From 652d11a89200bfdd5d692a2b1ebedbef0226d6b3 Mon Sep 17 00:00:00 2001 From: tupe12334 Date: Thu, 11 Jun 2026 23:08:20 +0300 Subject: [PATCH 3/4] apply cargo fmt to run.rs --- core/src/run.rs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/core/src/run.rs b/core/src/run.rs index f933560..79eb1b9 100644 --- a/core/src/run.rs +++ b/core/src/run.rs @@ -191,8 +191,7 @@ fn cleanup_session(steplock_dir: &Path, session_id: &str) -> Result<()> { if !fallback_path.exists() { return Ok(()); } - fs::read_to_string(&fallback_path) - .map(|s| s.trim().to_owned())? + fs::read_to_string(&fallback_path).map(|s| s.trim().to_owned())? }; let scope_dir = steplock_dir.join("sessions").join(&scope_key); if !scope_dir.exists() { @@ -688,10 +687,8 @@ reset = "always" }; let _ = run(&no_session, tmp.path()).unwrap(); - let fallback_id = fs::read_to_string( - tmp.path().join(".steplock/sessions/fallback-id"), - ) - .unwrap(); + let fallback_id = + fs::read_to_string(tmp.path().join(".steplock/sessions/fallback-id")).unwrap(); let fallback_id = fallback_id.trim(); let scope_dir = tmp.path().join(".steplock/sessions").join(fallback_id); assert!(scope_dir.exists()); From 44aa3223d5977a9188bbb0c1f218b628e0ec1ccd Mon Sep 17 00:00:00 2001 From: tupe12334 Date: Thu, 11 Jun 2026 23:09:32 +0300 Subject: [PATCH 4/4] update Architecture.md: session cleanup is now implemented --- Architecture.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Architecture.md b/Architecture.md index 56d7da5..588cf64 100644 --- a/Architecture.md +++ b/Architecture.md @@ -51,7 +51,7 @@ Commit `.steplock/checklists/` — these are your checklist definitions. Gitigno **Init** — lazy. First hook invocation for an unseen scope key creates the dir and initializes `state.json` with the start state. -**Cleanup** — polyhook emits `session:stop`. steplock removes `.steplock/sessions//`. (Cleanup on `session:stop` is planned — not yet implemented.) +**Cleanup** — polyhook emits `session:stop`. steplock removes `.steplock/sessions//`. ---