Skip to content

fix(sandbox): support claude-code profile extensions and simplify config - #691

Merged
lukehinds merged 4 commits into
mainfrom
claude-profile
Apr 16, 2026
Merged

fix(sandbox): support claude-code profile extensions and simplify config#691
lukehinds merged 4 commits into
mainfrom
claude-profile

Conversation

@lukehinds

Copy link
Copy Markdown
Contributor

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.

  • 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.

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>
@github-actions github-actions Bot added the bug Something isn't working label Apr 16, 2026

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread crates/nono-cli/src/sandbox_prepare.rs
@ctrlok

ctrlok commented Apr 16, 2026

Copy link
Copy Markdown

Hey! We can continue with current PR as is. I just think that we also can precreate $CLAUDE_CONFIG_DIR.lock directory, while it is expected to exist for both cases (with and without $CLAUDE_CONFIG_DIR set)

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 ~/.claude.lock/ if CLAUDE_CONFIG_DIR is not set and ~/$CLAUDE_CONFIG_DIR.lock/ if it was set)

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 --debug flag.

EPERM: operation not permitted, mkdir '/Users/sevapoliakov/.claude-second-profile.lock'

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>
@lukehinds

Copy link
Copy Markdown
Contributor Author

alright - does this look ok @ctrlok ?

@ctrlok

ctrlok commented Apr 16, 2026

Copy link
Copy Markdown

@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
b) update documentation to say that this folder should be created by user

@ctrlok

ctrlok commented Apr 16, 2026

Copy link
Copy Markdown

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

@lukehinds

Copy link
Copy Markdown
Contributor Author

a few more changes needed i just spotted.

Comment thread crates/nono-cli/src/sandbox_prepare.rs Outdated
// 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() {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
@lukehinds

Copy link
Copy Markdown
Contributor Author

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
before the env var is set) checks claude-code-credentials, finds tokens, thinks auth exists, doesn't enable launch services. But Claude Code sees the env var, looks under claude-code-<hash>-credentials, finds nothing, triggers login with no browser.

@ctrlok

ctrlok commented Apr 16, 2026

Copy link
Copy Markdown

@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 ~/.claude/.credentials.json (or ~/$CLAUDE_CONFIG_DIR/.credentials.json)

Probably different version of claude-code (I'm using from homebrew)

@lukehinds

Copy link
Copy Markdown
Contributor Author

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>
@lukehinds

Copy link
Copy Markdown
Contributor Author

ok, the profile check now walks the inheritance chain so any profile that extends claude-code gets the same setup - symlink redirect, file precreation, and launch services auto-enable.

clear creds and make sure brower open operation is triggerd:

image

follow up run shows auth still present

image

custom profile

image

test with CLAUDE_CONFIG_DIR

image

Check browser is allowed to open with CLAUDE_CONFIG_DIR

image

Non-claude profile doesn't touch anything

image

@lukehinds

Copy link
Copy Markdown
Contributor Author

I will get this shipped if sounds good to you @ctrlok

@lukehinds
lukehinds merged commit e4a7133 into main Apr 16, 2026
12 checks passed
@ctrlok

ctrlok commented Apr 17, 2026

Copy link
Copy Markdown

@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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants