@@ -275,6 +275,7 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
275275 permission_mode,
276276 base_commit,
277277 reasoning_effort,
278+ allow_broad_cwd,
278279 } => {
279280 ensure_hackcode_ready ( ) ?;
280281 check_for_updates_async ( ) ;
@@ -284,6 +285,7 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
284285 permission_mode,
285286 base_commit,
286287 reasoning_effort,
288+ allow_broad_cwd,
287289 ) ?;
288290 }
289291 CliAction :: HelpTopic ( topic) => print_help_topic ( topic) ,
@@ -1237,19 +1239,25 @@ fn run_self_update() -> Result<(), Box<dyn std::error::Error>> {
12371239 // Clone or pull
12381240 if src_dir. join ( ".git" ) . exists ( ) {
12391241 eprintln ! ( " {dim}Pulling latest...{nc}" ) ;
1240- let _ = Command :: new ( "git" )
1241- . args ( [ "-C" , & src_dir. to_string_lossy ( ) , "checkout" , "dev" , "--quiet" ] )
1242+ // Fetch and hard-reset to avoid divergent branch errors
1243+ let fetch = Command :: new ( "git" )
1244+ . args ( [ "-C" , & src_dir. to_string_lossy ( ) , "fetch" , "origin" , "dev" , "--quiet" ] )
12421245 . status ( ) ;
1243- let pull = Command :: new ( "git" )
1244- . args ( [ "-C" , & src_dir. to_string_lossy ( ) , "pull" , "--quiet" ] )
1245- . status ( ) ;
1246- if pull. map_or ( true , |s| !s. success ( ) ) {
1247- // If pull fails, re-clone
1246+ if fetch. map_or ( false , |s| s. success ( ) ) {
1247+ let _ = Command :: new ( "git" )
1248+ . args ( [ "-C" , & src_dir. to_string_lossy ( ) , "checkout" , "dev" , "--quiet" ] )
1249+ . status ( ) ;
1250+ let _ = Command :: new ( "git" )
1251+ . args ( [ "-C" , & src_dir. to_string_lossy ( ) , "reset" , "--hard" , "origin/dev" ] )
1252+ . status ( ) ;
1253+ } else {
1254+ // If fetch fails, re-clone
12481255 let _ = std:: fs:: remove_dir_all ( & src_dir) ;
12491256 let status = Command :: new ( "git" )
12501257 . args ( [
12511258 "clone" ,
12521259 "--quiet" ,
1260+ "--branch" , "dev" ,
12531261 "https://github.com/itwizardo/hackcode.git" ,
12541262 & src_dir. to_string_lossy ( ) ,
12551263 ] )
@@ -1823,49 +1831,28 @@ fn check_auth_health() -> DiagnosticCheck {
18231831 ) ;
18241832
18251833 match load_oauth_credentials ( ) {
1826- Ok ( Some ( token_set) ) => {
1827- let expired = oauth_token_is_expired ( & api:: OAuthTokenSet {
1828- access_token : token_set. access_token . clone ( ) ,
1829- refresh_token : token_set. refresh_token . clone ( ) ,
1830- expires_at : token_set. expires_at ,
1831- scopes : token_set. scopes . clone ( ) ,
1832- } ) ;
1833- let mut details = vec ! [
1834- format!(
1835- "Environment api_key={} auth_token={}" ,
1836- if api_key_present { "present" } else { "absent" } ,
1837- if auth_token_present {
1838- "present"
1839- } else {
1840- "absent"
1841- }
1842- ) ,
1843- format!(
1844- "Saved OAuth expires_at={} refresh_token={} scopes={}" ,
1845- token_set
1846- . expires_at
1847- . map_or_else( || "<none>" . to_string( ) , |value| value. to_string( ) ) ,
1848- if token_set. refresh_token. is_some( ) {
1849- "present"
1850- } else {
1851- "absent"
1852- } ,
1853- if token_set. scopes. is_empty( ) {
1854- "<none>" . to_string( )
1855- } else {
1856- token_set. scopes. join( "," )
1857- }
1858- ) ,
1859- ] ;
1860- if expired {
1861- details. push (
1862- "Suggested action hackcodelogin to refresh local OAuth credentials" . to_string ( ) ,
1863- ) ;
1864- }
1865- DiagnosticCheck :: new (
1866- "Auth" ,
1867- if expired {
1868- DiagnosticLevel :: Warn
1834+ Ok ( Some ( token_set) ) => DiagnosticCheck :: new (
1835+ "Auth" ,
1836+ if api_key_present || auth_token_present {
1837+ DiagnosticLevel :: Ok
1838+ } else {
1839+ DiagnosticLevel :: Warn
1840+ } ,
1841+ if api_key_present || auth_token_present {
1842+ "supported auth env vars are configured; legacy saved OAuth is ignored"
1843+ } else {
1844+ "legacy saved OAuth credentials are present but unsupported"
1845+ } ,
1846+ )
1847+ . with_details ( vec ! [
1848+ env_details,
1849+ format!(
1850+ "Legacy OAuth expires_at={} refresh_token={} scopes={}" ,
1851+ token_set
1852+ . expires_at
1853+ . map_or_else( || "<none>" . to_string( ) , |value| value. to_string( ) ) ,
1854+ if token_set. refresh_token. is_some( ) {
1855+ "present"
18691856 } else {
18701857 "absent"
18711858 } ,
@@ -3273,6 +3260,78 @@ fn run_stale_base_preflight(flag_value: Option<&str>) {
32733260 }
32743261}
32753262
3263+ fn detect_broad_cwd ( ) -> Option < PathBuf > {
3264+ let Ok ( cwd) = env:: current_dir ( ) else {
3265+ return None ;
3266+ } ;
3267+ let is_home = env:: var_os ( "HOME" )
3268+ . or_else ( || env:: var_os ( "USERPROFILE" ) )
3269+ . is_some_and ( |h| Path :: new ( & h) == cwd) ;
3270+ let is_root = cwd. parent ( ) . is_none ( ) ;
3271+ if is_home || is_root {
3272+ Some ( cwd)
3273+ } else {
3274+ None
3275+ }
3276+ }
3277+
3278+ fn enforce_broad_cwd_policy (
3279+ allow_broad_cwd : bool ,
3280+ output_format : CliOutputFormat ,
3281+ ) -> Result < ( ) , Box < dyn std:: error:: Error > > {
3282+ if allow_broad_cwd {
3283+ return Ok ( ( ) ) ;
3284+ }
3285+ let Some ( cwd) = detect_broad_cwd ( ) else {
3286+ return Ok ( ( ) ) ;
3287+ } ;
3288+
3289+ let is_interactive = io:: stdin ( ) . is_terminal ( ) ;
3290+
3291+ if is_interactive {
3292+ eprintln ! (
3293+ "Warning: hackcode is running from a very broad directory ({}).\n \
3294+ The agent can read and search everything under this path.\n \
3295+ Consider running from inside your project: cd /path/to/project && hackcode",
3296+ cwd. display( )
3297+ ) ;
3298+ eprint ! ( "Continue anyway? [y/N]: " ) ;
3299+ io:: stderr ( ) . flush ( ) ?;
3300+
3301+ let mut input = String :: new ( ) ;
3302+ io:: stdin ( ) . read_line ( & mut input) ?;
3303+ let trimmed = input. trim ( ) . to_lowercase ( ) ;
3304+ if trimmed != "y" && trimmed != "yes" {
3305+ eprintln ! ( "Aborted." ) ;
3306+ std:: process:: exit ( 0 ) ;
3307+ }
3308+ Ok ( ( ) )
3309+ } else {
3310+ let message = format ! (
3311+ "hackcode is running from a very broad directory ({}). \
3312+ The agent can read and search everything under this path. \
3313+ Use --allow-broad-cwd to proceed anyway, \
3314+ or run from inside your project: cd /path/to/project && hackcode",
3315+ cwd. display( )
3316+ ) ;
3317+ match output_format {
3318+ CliOutputFormat :: Json => {
3319+ eprintln ! (
3320+ "{}" ,
3321+ serde_json:: json!( {
3322+ "type" : "error" ,
3323+ "error" : message,
3324+ } )
3325+ ) ;
3326+ }
3327+ CliOutputFormat :: Text => {
3328+ eprintln ! ( "error: {message}" ) ;
3329+ }
3330+ }
3331+ std:: process:: exit ( 1 ) ;
3332+ }
3333+ }
3334+
32763335#[ allow( clippy:: needless_pass_by_value) ]
32773336fn run_repl (
32783337 model : String ,
@@ -5025,6 +5084,7 @@ fn collect_sessions_from_dir(
50255084 sessions. push ( ManagedSessionSummary {
50265085 id,
50275086 path,
5087+ updated_at_ms : modified_epoch_millis as u64 ,
50285088 modified_epoch_millis,
50295089 message_count,
50305090 parent_session_id,
0 commit comments