From d02260a5d63e90ffb4381564fbe7c03f5361a8e4 Mon Sep 17 00:00:00 2001 From: Seva Poliakov Date: Wed, 22 Apr 2026 10:11:12 +0300 Subject: [PATCH] fix CLAUDE_CONFIG_DIR and add precreation of lock files and refreshes for claude-no-kc profile Signed-off-by: Seva Poliakov --- crates/nono-cli/src/sandbox_prepare.rs | 99 ++++++++++++++++---------- 1 file changed, 63 insertions(+), 36 deletions(-) diff --git a/crates/nono-cli/src/sandbox_prepare.rs b/crates/nono-cli/src/sandbox_prepare.rs index cbbe05fee..b7e6a5d02 100644 --- a/crates/nono-cli/src/sandbox_prepare.rs +++ b/crates/nono-cli/src/sandbox_prepare.rs @@ -35,10 +35,11 @@ fn print_allow_domain_port_warnings(entries: &[String], context: &str, silent: b } } -/// Returns `true` if `profile_name` is `"claude-code"` or transitively extends it. +/// Returns `true` if `profile_name` is `"claude-code"` or `"claude-no-kc"`, or +/// transitively extends either. fn is_claude_code_profile(profile_name: &str) -> bool { fn check(name: &str, visited: &mut Vec) -> bool { - if name == "claude-code" { + if name == "claude-code" || name == "claude-no-kc" { return true; } if visited.iter().any(|v| v == name) { @@ -1074,45 +1075,71 @@ pub(crate) fn prepare_sandbox(args: &SandboxArgs, silent: bool) -> Result.. Landlock/Seatbelt cannot - // grant permission for these dynamically-named files in ~/, so token - // refreshes silently fail and the user is logged out. - // - // Fix: redirect ~/.claude.json to ~/.claude/claude.json via a - // symlink. Claude Code resolves symlinks before computing the temp - // file path, so temp files land in ~/.claude/ (already readwrite) - // instead of ~/ (not writable inside the sandbox). - let claude_json = home_path.join(".claude.json"); - let claude_dir = home_path.join(".claude"); - let redirect_target = claude_dir.join("claude.json"); - - if let Err(e) = std::fs::create_dir_all(&claude_dir) { - warn!("Failed to create ~/.claude: {}", e); - } else if !claude_json.is_symlink() { - if claude_json.exists() { - // Regular file present — move it into ~/.claude/ then symlink. - if let Err(e) = std::fs::rename(&claude_json, &redirect_target) { - warn!( - "Failed to move ~/.claude.json to ~/.claude/claude.json: {}", - e - ); - } else if let Err(e) = - std::os::unix::fs::symlink(".claude/claude.json", &claude_json) - { - warn!("Failed to create ~/.claude.json symlink: {}", e); - } - } else { - // File doesn't exist yet — pre-create the target so the - // sandbox can attach a path rule to it, then symlink. - precreate(&redirect_target, false); - if let Err(e) = std::os::unix::fs::symlink(".claude/claude.json", &claude_json) { - if e.kind() != std::io::ErrorKind::AlreadyExists { + // When CLAUDE_CONFIG_DIR is set, Claude Code reads/writes + // $CLAUDE_CONFIG_DIR/.claude.json directly and ignores ~/.claude.json. + // The symlink redirect is only relevant for the default-path case. + // The caller is expected to `--allow $CLAUDE_CONFIG_DIR` separately. + if std::env::var_os("CLAUDE_CONFIG_DIR").is_none() { + // Claude Code writes ~/.claude.json atomically via temp files named + // ~/.claude.json.tmp... Landlock/Seatbelt cannot + // grant permission for these dynamically-named files in ~/, so + // token refreshes silently fail and the user is logged out. + // + // Fix: redirect ~/.claude.json to ~/.claude/claude.json via a + // symlink. Claude Code resolves symlinks before computing the + // temp file path, so temp files land in ~/.claude/ (already + // readwrite) instead of ~/ (not writable inside the sandbox). + let claude_json = home_path.join(".claude.json"); + let claude_dir = home_path.join(".claude"); + let redirect_target = claude_dir.join("claude.json"); + + if let Err(e) = std::fs::create_dir_all(&claude_dir) { + warn!("Failed to create ~/.claude: {}", e); + } else if !claude_json.is_symlink() { + if claude_json.exists() { + // Regular file present — move it into ~/.claude/ then symlink. + if let Err(e) = std::fs::rename(&claude_json, &redirect_target) { + warn!( + "Failed to move ~/.claude.json to ~/.claude/claude.json: {}", + e + ); + } else if let Err(e) = + std::os::unix::fs::symlink(".claude/claude.json", &claude_json) + { warn!("Failed to create ~/.claude.json symlink: {}", e); } + } else { + // File doesn't exist yet — pre-create the target so the + // sandbox can attach a path rule to it, then symlink. + precreate(&redirect_target, false); + if let Err(e) = + std::os::unix::fs::symlink(".claude/claude.json", &claude_json) + { + if e.kind() != std::io::ErrorKind::AlreadyExists { + warn!("Failed to create ~/.claude.json symlink: {}", e); + } + } } } } + + // Precreate Claude Code's token-refresh lock directory at the path + // Claude Code actually uses: "${effective_config_dir}.lock/" where + // effective_config_dir = $CLAUDE_CONFIG_DIR if set, else ~/.claude. + // Claude Code mkdirs this to acquire the lock and rmdirs it afterwards; + // the sandbox can only attach a path rule to an existing inode, so + // pre-create it on every run. Use string concatenation (not + // `with_extension`) to match Claude Code's "${config_dir}.lock" + // naming — `with_extension` would replace a pre-existing extension. + let lock_base = std::env::var_os("CLAUDE_CONFIG_DIR") + .map(PathBuf::from) + .unwrap_or_else(|| home_path.join(".claude")); + let lock_dir = { + let mut s = lock_base.into_os_string(); + s.push(".lock"); + PathBuf::from(s) + }; + precreate(&lock_dir, true); } let (mut caps, needs_unlink_overrides) = if let Some(ref profile) = loaded_profile {