fix(sandbox): support claude-code profile extensions and simplify config - #691
Conversation
Issue discovered and reported by @ctrlok - Introduce `is_claude_code_profile` to correctly identify profiles that are "claude-code" or transitively extend it. - Update `should_auto_enable_claude_launch_services` and `prepare_sandbox` to use this new profile check. - Replace the symlink-based redirection for `~/.claude.json` with setting the `CLAUDE_CONFIG_DIR` environment variable to `~/.claude`. - This ensures Claude Code writes all its config and temporary files (used for atomic writes during token refresh) into a directory (`~/.claude/`) that the sandbox already grants read/write permissions to. - This resolves silently failing token refreshes due to temporary files being created in a non-writable home directory. Co-authored-by: Seva Poliakov <seva@ctrlok.com> Signed-off-by: Luke Hinds <lukehinds@gmail.com>
There was a problem hiding this comment.
Code Review
This pull request introduces a recursive check to determine if a profile extends 'claude-code' and simplifies the sandbox preparation logic by replacing symlink-based redirects with the CLAUDE_CONFIG_DIR environment variable. A potential denial-of-service vulnerability was identified in the new is_claude_code_profile function, as it lacks cycle detection for circular profile dependencies, which could lead to a stack overflow.
|
Hey! We can continue with current PR as is. I just think that we also can precreate Also, I'll take a look on login issues, while I had problems with login as well and it was caused by two different issues (one of them was access and existance of directory I mean this directory and access to it is directly related to claude-code ability to refresh token and not to ask for login every day. It can be proved by running claude inside nono with this lock directory is used only for token refresh as far as I traced. |
The `is_claude_code_profile` function previously used a simple recursive approach to determine if a profile transitively extends "claude-code". - This approach was vulnerable to infinite recursion and stack overflow if profile extension definitions formed a cyclical dependency (e.g., profile A extends profile B, and profile B extends profile A). - Introduce a `visited` vector within a nested helper function to track profiles that have already been processed in the current call stack. - If a profile already in the `visited` list is encountered, the function now bails out, effectively breaking the cycle and preventing an infinite loop. Signed-off-by: Luke Hinds <lukehinds@gmail.com>
|
alright - does this look ok @ctrlok ? |
|
@lukehinds it looks ok to me, still to fix 'ask for login every day' we need to choose from: a) precreating folder $CLAUDE_CONFIG_DIR.lock instead of user |
|
actually I was wrong, we need to precreate $CLAUDE_CONFIG_DIR - while it removes it right after a token refresh, so next day there will be problem again. Probably it can be fixed in a future nono version with prehook support, but for fix in the current version I'm totally sure that we need to precreate this folder |
|
a few more changes needed i just spotted. |
| // inside ~/.claude/ (which the sandbox already grants readwrite). | ||
| // This replaces the old symlink-based redirect for ~/.claude.json | ||
| // and also ensures token refresh temp files land in a writable dir. | ||
| if std::env::var_os("CLAUDE_CONFIG_DIR").is_none() { |
There was a problem hiding this comment.
I think it can be updated to something like this:
let dir: PathBuf = match std::env::var_os("CLAUDE_CONFIG_DIR") {
Some(val) => PathBuf::from(val),
None => {
let p = home_path.join(".claude");
#[allow(clippy::disallowed_methods)] // Single-threaded before fork.
std::env::set_var("CLAUDE_CONFIG_DIR", &p);
p
}
};
let mut lock = dir.clone().into_os_string();
lock.push(".lock");
precreate(Path::new(&lock), true);I tested it on my local build and it works just fine
then only issue - that we need to be sure that we will not create lock folders for users who do not run claude-related profiles
There was a problem hiding this comment.
Yes, it's safe. the CLAUDE_CONFIG_DIR set, the lock dir precreation, the cache dir precreation is inside the refactored if args.profile.as_deref().is_some_and(is_claude_code_profile) block. If you're running --profile default, --profile opencode, or no profile at all, none of this executes
I also fixed my mistake, by moving ~/.claude.lock from allow_file to allow in the policy so it's treated as a directory and move precreation of the lock directory before the sandbox kicks in
added some docs as well, just to make sure its there for others.
Claude Code creates a temporary directory `$CLAUDE_CONFIG_DIR.lock/` for token refreshing. The sandbox would block the creation of this directory. - Update the built-in `claude-code` profile to explicitly allow access to `$HOME/.claude.lock` as a directory rather than a file. - Automatically pre-create the `$CLAUDE_CONFIG_DIR.lock` directory in `sandbox_prepare` before launching Claude Code. This prevents sandbox violations when the client attempts to create it. - Document how `CLAUDE_CONFIG_DIR` is handled and provide guidance for custom configurations. Signed-off-by: Luke Hinds <lukehinds@gmail.com>
|
ah crap, I found another issue - going to need to think about this one a little more. when CLAUDE_CONFIG_DIR is explicitly set, Claude Code hashes it into the Keychain service name. So nono's auth detection (runs |
|
@lukehinds interesting, I don't have this issue at all, and I'm pretty sure my nono profile don't have access to keychain at all and token with refresh token is stored in the Probably different version of claude-code (I'm using from homebrew) |
this is one of the issues, they use three different installers (bun, shell and brew). |
Claude Code performs atomic writes to `~/.claude.json` using dynamically named temporary files (e.g., `~/.claude.json.tmp.<pid>.<timestamp>`). Sandboxes like Landlock and Seatbelt cannot grant write permissions for these unpredictable filenames directly in the user's home directory, causing token refreshes to silently fail. This change fixes the issue by: - Redirecting `~/.claude.json` to `~/.claude/claude.json` via a symlink. Claude Code resolves symlinks before creating temporary files, ensuring temporary files land in the `~/.claude/` directory, which is already writable by the sandbox. - Handling existing `~/.claude.json` files by moving them to `~/.claude/claude.json`. - Pre-creating `~/.claude/claude.json` if it doesn't exist, to ensure sandbox path rules can attach. - Pre-creating `~/.claude.json.lock`, which Claude Code also uses in the home directory. - Updating documentation to reflect these changes, including removing details about nono automatically setting `CLAUDE_CONFIG_DIR` and adding a note advising against explicitly setting `CLAUDE_CONFIG_DIR` to `~/.claude`. Signed-off-by: Luke Hinds <lukehinds@gmail.com>
|
I will get this shipped if sounds good to you @ctrlok |
|
@lukehinds hey! Your latest commit break $CLAUDE_CONFIG_DIR support. I'd love to revert to // Set CLAUDE_CONFIG_DIR so Claude Code writes config and lock files
// inside ~/.claude/ (which the sandbox already grants readwrite).
// This replaces the old symlink-based redirect for ~/.claude.json
// and also ensures token refresh temp files land in a writable dir.
let claude_config_dir = match std::env::var_os("CLAUDE_CONFIG_DIR") {
Some(dir) => PathBuf::from(dir),
None => {
let dir = home_path.join(".claude");
#[allow(clippy::disallowed_methods)] // Single-threaded before fork.
std::env::set_var("CLAUDE_CONFIG_DIR", &dir);
dir
}
};
// Claude Code creates $CLAUDE_CONFIG_DIR.lock/ for token refresh
// locking, then removes it afterwards. The sandbox blocks mkdir in
// the parent directory, so pre-create it each time.
let lock_dir = claude_config_dir.with_extension("lock");
precreate(&lock_dir, true);and remove all symlink based logic. Can you please write down what issues you had with the previous approach we did and I'll try to reproduce and fix it. |






Issue discovered and reported by @ctrlok
@ctrlok you can use this in yours, or we can merge this, I have added co-author as I don't want to deny you the opportunity to contribute.
is_claude_code_profileto correctly identify profiles that are "claude-code" or transitively extend it.should_auto_enable_claude_launch_servicesandprepare_sandboxto use this new profile check.~/.claude.jsonwith setting theCLAUDE_CONFIG_DIRenvironment variable to~/.claude.~/.claude/) that the sandbox already grants read/write permissions to.