Skip to content

feat(credential-broker): add macos keychain credential broker#1034

Draft
lukehinds wants to merge 2 commits into
mainfrom
keychain-broker-prototype
Draft

feat(credential-broker): add macos keychain credential broker#1034
lukehinds wants to merge 2 commits into
mainfrom
keychain-broker-prototype

Conversation

@lukehinds

Copy link
Copy Markdown
Contributor

Introduces a new system for brokering macOS Keychain access, that allows nono profiles to define granular permissions for security CLI interactions. This brings in a new credential-helper internal subcommand processes credential requests via supervisor IPC. On macOS, a security shim is installed in PATH when NONO_EXPERIMENTAL_KEYCHAIN_BROKER is set, redirecting keychain operations through nono. Profiles can now specify credential_access grants, including provider, classes, operations, services, service_prefixes, and accounts to control access.

Introduces a new system for brokering macOS Keychain access for sandboxed applications.

- This feature allows `nono` profiles to define granular permissions for `security` CLI interactions.
- A new `credential-helper` internal subcommand processes credential requests via supervisor IPC.
- On macOS, a `security` shim is installed in `PATH` when `NONO_EXPERIMENTAL_KEYCHAIN_BROKER` is set, redirecting keychain operations through `nono`.
- Profiles can now specify `credential_access` grants, including `provider`, `classes`, `operations`, `services`, `service_prefixes`, and `accounts` to control access.

Signed-off-by: Luke Hinds <lukehinds@gmail.com>
@github-actions

github-actions Bot commented May 27, 2026

Copy link
Copy Markdown
Contributor

PR Review Summary

Size

Metric Value
Lines added +1142
Lines removed -13
Total changed 1155
Classification Large (> 300 lines)

Affected crates

  • crates/nono (core library) — careful review required. This is the security-critical sandbox primitive. A bug here bypasses OS-level isolation for every downstream user.
  • crates/nono-cli — CLI changes. Verify argument parsing, flag documentation, and UX behaviour across supported platforms.

Blast radius — Contained

This PR touches: source code


Updated automatically on each push to this PR.

@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 supervisor-brokered credential helper for macOS Keychain access, adding a new credential-helper subcommand, a credential_broker module to parse and authorize macOS security CLI arguments, and support for creating a security PATH shim to intercept keychain requests inside the supervised sandbox. The code review feedback highlights several important improvements: writing raw bytes directly to stdout instead of using String::from_utf8 and println! to avoid restricting secrets to UTF-8 and appending unwanted newlines; using a symlink or hard link instead of copying the entire nono executable to reduce I/O overhead; checking if stdin is a TTY before calling read_to_string to prevent indefinite blocking; correctly handling passwords starting with a dash for the -w flag; and adding support for the -g flag in security find-generic-password to improve compatibility with existing scripts.

Comment thread crates/nono-cli/src/credential_broker.rs Outdated
Comment thread crates/nono-cli/src/exec_strategy.rs
Comment thread crates/nono-cli/src/credential_broker.rs
Comment on lines +416 to +422
"-w" => {
if matches!(args.get(i + 1), Some(value) if !value.starts_with('-')) {
i += 1;
flags.secret = Some(args[i].as_bytes().to_vec());
} else {
flags.print_password = true;
}

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.

medium

In security add-generic-password, the -w flag always requires a value (the password to add). However, the generic parse_security_flags parser treats -w as a boolean flag if the next argument starts with - (e.g., if the password itself starts with a dash). This will cause passwords starting with a dash to be parsed incorrectly.

Consider passing a context or boolean parameter to parse_security_flags indicating whether -w requires a value, or separate the parsing logic for add and find commands.

Comment on lines +435 to +436
"-i" => return Err("security -i must be the top-level invocation".to_string()),
other => return Err(format!("unsupported security flag: {other}")),

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.

medium

The standard macOS security find-generic-password command historically uses the -g flag to display/print the password. Many existing scripts rely on -g rather than -w.

Since -g is currently unhandled, any invocation using -g will fail with an unsupported security flag error. Consider adding support for -g (mapping it to print_password = true) to improve compatibility with existing scripts.

            "-g" => {
                flags.print_password = true;
            }
            "-i" => return Err("security -i must be the top-level invocation".to_string()),
            other => return Err(format!("unsupported security flag: {other}")),

@lukehinds

lukehinds commented May 28, 2026

Copy link
Copy Markdown
Contributor Author

Still need some work, but pleased to see no dreaded red labels from gemmy.

@lukehinds lukehinds linked an issue May 28, 2026 that may be closed by this pull request
…ling

The `SecretBytes` type (an alias for `Zeroizing<Vec<u8>>`) is now used for all secret material passed through the supervisor IPC and handled by the credential broker.

This change ensures that sensitive credential data is securely wiped from memory when the `SecretBytes` object is dropped, reducing the risk of accidental secret leakage.

Additionally, the `nono credential` command now writes raw binary secret data directly to stdout, supporting non-UTF8 secrets and preventing potential issues with string conversions.

Signed-off-by: Luke Hinds <lukehinds@gmail.com>
@TBeijen

TBeijen commented Jun 1, 2026

Copy link
Copy Markdown

Ran some tests as we discussed in Discord.

See: https://github.com/TBeijen/nono-pocs/blob/main/broker-test/RESULT-20260531-0820CEST.txt (and the .sh files around it)

TL;DR:

  • Intercepts CLI keychain requests that are allowed, triggers OS popup where I need to confirm by entering pw.
  • Blocks CLI keychain requests not explicitly allowed
  • Only intercepts security cli, does not intercept gh retrieving credentials from Keychain

In its current form could work, by allowing only access to claude credentials (via cli shim) while blocking access to Keychain in general. As long as Claude doesn't start using Security.framework.

I have no SSO account for Claude to actually test its way of storing those credentials.

(🤖 / 👨):
GitHub CLI uses go-keyring, calling SecItemCopyMatching/SecItemAdd via Security.framework. So that would still require blocking KeyChain access entirely.

No originally Claude believed claude used keytar to access Keychain in a similar way to gh, based on it diggin through itself using strings. But when pressed harder, since keytar is archived, it came to the following conclusion:

I was wrong. Claude Code does use the security CLI, not native SecItem* APIs directly.

This is definitive. Claude Code shells out to the security CLI for all Keychain operations:

  - Read: security find-generic-password -a "<account>" -w -s "<service>"
  - Write: security add-generic-password -U -a <account> -s <service> -X <hex-data> (or via security -i stdin for large payloads)
  - Delete: security delete-generic-password -a <account> -s <service>
  - Probe: security show-keychain-info

  The SecItemCopyMatching/SecItemAdd symbols I found earlier are just in the compiled security binary strings or framework metadata baked into the single binary — they're not called
   directly by Claude Code's Node.js runtime.

  This is great news for the credential broker. It means nono's existing shim approach (replacing security in PATH) would work for Claude Code's OAuth tokens. The architecture you
  asked about earlier — block Keychain by default, intercept via shim — is exactly what would work here:

  1. Block Keychain at Mach level (deny_keychains_macos)
  2. Shim security CLI in PATH (broker already does this)
  3. Claude's /login OAuth calls go through the shim → broker validates against credential_access policy → supervisor (unsandboxed) executes the real security command

  The gh CLI remains the odd one out since it uses go-keyring (native APIs). But for Claude itself, the broker approach is viable. The --env-credential / GH_TOKEN injection handles
  gh.

@TBeijen

TBeijen commented Jun 1, 2026

Copy link
Copy Markdown

If we phrase the goal as:

Don't mind claude accessing it's own SSO credentials, but don't grant it access to anything else in Keychain

Then there might be some pointers in thise OpenShell issue which tackles the same problem: NVIDIA/OpenShell#620

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Supervised macOS Keychain Broker

2 participants