@@ -172,7 +172,9 @@ fn wire_claude_md(path: &Path) -> Result<bool> {
172172 format ! ( "{existing}\n \n {CLAUDE_MD_BLOCK}" )
173173 } ;
174174
175- std:: fs:: write ( path, new_content) . with_context ( || format ! ( "write {}" , path. display( ) ) ) ?;
175+ let tmp = path. with_extension ( "md.tmp" ) ;
176+ std:: fs:: write ( & tmp, & new_content) . with_context ( || format ! ( "write {}" , tmp. display( ) ) ) ?;
177+ std:: fs:: rename ( & tmp, path) . with_context ( || format ! ( "rename to {}" , path. display( ) ) ) ?;
176178 Ok ( true )
177179}
178180
@@ -328,26 +330,21 @@ fn cmd_index() -> Result<()> {
328330
329331 save_index ( & existing) ?;
330332
333+ println ! (
334+ "Indexed: {} new, {} updated, {} unchanged, {} pruned{} ({} total)" ,
335+ new_count,
336+ updated_count,
337+ unchanged_count,
338+ pruned,
339+ if error_count > 0 {
340+ format!( ", {} errors" , error_count)
341+ } else {
342+ String :: new( )
343+ } ,
344+ existing. len( )
345+ ) ;
331346 if error_count > 0 {
332- println ! (
333- "Indexed: {} new, {} updated, {} unchanged, {} pruned, {} errors ({} total)" ,
334- new_count,
335- updated_count,
336- unchanged_count,
337- pruned,
338- error_count,
339- existing. len( )
340- ) ;
341- std:: process:: exit ( 1 ) ;
342- } else {
343- println ! (
344- "Indexed: {} new, {} updated, {} unchanged, {} pruned ({} total)" ,
345- new_count,
346- updated_count,
347- unchanged_count,
348- pruned,
349- existing. len( )
350- ) ;
347+ anyhow:: bail!( "{error_count} file(s) could not be read" ) ;
351348 }
352349 Ok ( ( ) )
353350}
@@ -425,7 +422,10 @@ fn save_index(entries: &[IndexEntry]) -> Result<()> {
425422 let tmp = path. with_extension ( "json.tmp" ) ;
426423 std:: fs:: write ( & tmp, serde_json:: to_string ( entries) ?)
427424 . with_context ( || format ! ( "write {}" , tmp. display( ) ) ) ?;
428- std:: fs:: rename ( & tmp, & path) . with_context ( || format ! ( "rename to {}" , path. display( ) ) ) ?;
425+ if let Err ( e) = std:: fs:: rename ( & tmp, & path) {
426+ let _ = std:: fs:: remove_file ( & tmp) ;
427+ return Err ( e) . with_context ( || format ! ( "rename to {}" , path. display( ) ) ) ;
428+ }
429429 Ok ( ( ) )
430430}
431431
@@ -469,7 +469,13 @@ fn find_memory_md(cwd: &Path) -> Option<(String, PathBuf)> {
469469 }
470470 // Strategy 2: ~/.claude/projects/<encoded>/memory/MEMORY.md
471471 let projects = dirs:: home_dir ( ) ?. join ( ".claude" ) . join ( "projects" ) ;
472- let canonical = std:: fs:: canonicalize ( cwd) . ok ( ) ?;
472+ let canonical = match std:: fs:: canonicalize ( cwd) {
473+ Ok ( p) => p,
474+ Err ( e) => {
475+ eprintln ! ( "mem: cannot canonicalize {}: {e}" , cwd. display( ) ) ;
476+ return None ;
477+ }
478+ } ;
473479 let encoded = "-" . to_string ( )
474480 + & canonical
475481 . to_string_lossy ( )
@@ -512,17 +518,22 @@ fn file_mtime(path: &Path) -> i64 {
512518}
513519
514520fn hook_command_exists ( entry : & serde_json:: Value , cmd : & str ) -> bool {
515- entry. as_array ( ) . is_some_and ( |arr| {
516- arr. iter ( ) . any ( |item| {
521+ session_start_commands ( entry) . any ( |c| c == cmd)
522+ }
523+
524+ /// Iterator over every `command` string inside a SessionStart hook array.
525+ fn session_start_commands < ' a > ( entry : & ' a serde_json:: Value ) -> impl Iterator < Item = & ' a str > + ' a {
526+ entry
527+ . as_array ( )
528+ . into_iter ( )
529+ . flat_map ( |arr| arr. iter ( ) )
530+ . flat_map ( |item| {
517531 item. get ( "hooks" )
518532 . and_then ( |h| h. as_array ( ) )
519- . is_some_and ( |hooks| {
520- hooks
521- . iter ( )
522- . any ( |h| h. get ( "command" ) . and_then ( |c| c. as_str ( ) ) == Some ( cmd) )
523- } )
533+ . into_iter ( )
534+ . flat_map ( |hooks| hooks. iter ( ) )
524535 } )
525- } )
536+ . filter_map ( |h| h . get ( "command" ) . and_then ( |c| c . as_str ( ) ) )
526537}
527538
528539fn atomic_write_json ( path : & Path , value : & serde_json:: Value ) -> Result < ( ) > {
@@ -540,23 +551,13 @@ fn check_session_start_hook(settings_path: &Path) -> &'static str {
540551 let Ok ( val) = serde_json:: from_str :: < serde_json:: Value > ( & raw ) else {
541552 return "malformed settings.json" ;
542553 } ;
543- let has_hook = val
554+ let entry = val
544555 . get ( "hooks" )
545556 . and_then ( |h| h. get ( "SessionStart" ) )
546- . and_then ( |s| s. as_array ( ) )
547- . is_some_and ( |arr| {
548- arr. iter ( ) . any ( |item| {
549- item. get ( "hooks" )
550- . and_then ( |h| h. as_array ( ) )
551- . is_some_and ( |hooks| {
552- hooks. iter ( ) . any ( |h| {
553- h. get ( "command" )
554- . and_then ( |c| c. as_str ( ) )
555- . is_some_and ( |c| c. ends_with ( " session-start" ) )
556- } )
557- } )
558- } )
559- } ) ;
557+ . cloned ( )
558+ . unwrap_or ( serde_json:: Value :: Array ( vec ! [ ] ) ) ;
559+ // Accept any command ending with " session-start" to handle path changes after reinstall.
560+ let has_hook = session_start_commands ( & entry) . any ( |c| c. ends_with ( " session-start" ) ) ;
560561 if has_hook {
561562 "installed"
562563 } else {
@@ -690,6 +691,27 @@ mod tests {
690691 assert_eq ! ( loaded[ 0 ] . content, "- Used JWT for auth" ) ;
691692 }
692693
694+ #[ test]
695+ fn resolve_cwd_uses_project_override ( ) {
696+ let tmp = tempfile:: tempdir ( ) . unwrap ( ) ;
697+ let result = resolve_cwd ( Some ( tmp. path ( ) . to_path_buf ( ) ) ) . unwrap ( ) ;
698+ assert_eq ! ( result, tmp. path( ) ) ;
699+ }
700+
701+ #[ test]
702+ fn hook_stdin_parses_cwd_field ( ) {
703+ let json = r#"{"cwd":"/tmp/myproject","sessionId":"abc"}"# ;
704+ let parsed: HookStdin = serde_json:: from_str ( json) . unwrap ( ) ;
705+ assert_eq ! ( parsed. cwd. as_deref( ) , Some ( "/tmp/myproject" ) ) ;
706+ }
707+
708+ #[ test]
709+ fn hook_stdin_missing_cwd_is_none ( ) {
710+ let json = r#"{"sessionId":"abc"}"# ;
711+ let parsed: HookStdin = serde_json:: from_str ( json) . unwrap ( ) ;
712+ assert ! ( parsed. cwd. is_none( ) ) ;
713+ }
714+
693715 #[ test]
694716 fn search_matches_lines_case_insensitive ( ) {
695717 let entries = vec ! [ IndexEntry {
0 commit comments