AI agents often need authenticated access to CLIs, APIs, and browser sessions, but plaintext credentials should never enter prompts, model context, or tool results.
SecretBroker is a local macOS credential broker for AI agents. It lets agents request scoped, user-approved operations through MCP and injects credentials into an approved process or broker-owned proxy instead of returning stored values to the agent or model.
Warning
SecretBroker handles high-value secrets and has not received an independent security audit. It is not a sandbox: an approved command runs with the user's permissions and can deliberately print or exfiltrate injected credentials. Read Security and SECURITY.md before using real accounts.
Important
Build the macOS app from source. Before building or installing it, verify the repository origin and the commit you checked out. Treat third-party binaries as untrusted unless you can verify their publisher identity, notarization, and checksum.
- Imports live or snapshot cookies from Chrome, Chromium, Brave, Edge, Vivaldi, and Arc.
- Stores secrets in a Keychain-backed AES-GCM vault.
- Supports per-request, session, time-window, and automatic approval policies.
- Provides MCP tools for AI agents to discover scopes and request approved credential operations.
- Exposes a CLI and a scope-locked reverse proxy for local automation.
- Provides Playwright storage state, Netscape cookie jars, and cookie header files.
- Records credential operations in a hash-chained audit log.
- Revokes grants and removes temporary credential material with one panic action.
- Supports multiple accounts and multiple scopes per credential.
- Loads long-lived External Secrets from secret managers or other remote stores into daemon memory.
- Supports user-configured command providers for short-lived secrets, including visible Terminal login flows.
- macOS 13 or later
- Xcode or matching Command Line Tools with Swift 6 support
- Chrome or another supported Chromium browser for cookie import
- A Developer ID Application certificate for an installed release build
- An MCP-capable AI agent host, such as Codex or Claude Code, only if you want to use the MCP integration
SecretBroker has no third-party package dependencies. It uses macOS frameworks, SQLite, and CommonCrypto.
Production peer authentication requires every SecretBroker component to share one Developer ID Application signing identity. Clone the repository and run the installer with that identity:
git clone https://github.com/thonra/secretbroker.git
cd secretbroker
SECRETBROKER_SIGNING_IDENTITY="Developer ID Application: Example (TEAMID)" \
./install.shThe installer builds the project, installs the binaries under
~/Library/Application Support/SecretBroker, installs the menu-bar app under
~/Applications, and registers the app as a login LaunchAgent. The app starts
and owns secretbrokerd; quitting or losing the app stops the daemon and clears
ephemeral broker state. The App LaunchAgent is not KeepAlive, so choosing Quit
does not relaunch it. While the app remains open, it restarts an unexpectedly
exited daemon; rapid startup failures stop after five retries. Quit the app to
stop the daemon intentionally. The installer refuses
ad-hoc signing and refuses to copy a plaintext development master key into a
backup. Migrate or deliberately remove a development vault before installing.
It does not change any MCP host configuration; register the installed MCP binary
after installation if needed.
When only the legacy ~/Library/Application Support/KeyBroker directory exists,
install.sh moves it to the SecretBroker support path before installing the new
binaries. The daemon migrates the existing com.keybroker.vault Keychain key to
com.secretbroker.vault, so the encrypted vault remains usable. If both support
directories exist, the installer stops instead of choosing one implicitly;
reconcile them before retrying. MCP host registrations are not rewritten, so
update them to point to secretbroker-mcp.
For compatibility, SecretBroker still accepts legacy KEYBROKER_* environment
variables, kb_* MCP tool calls, X-KeyBroker-Token, and signed
com.keybroker.* clients. New names take precedence and all new configuration
should use SECRETBROKER_*, sb_*, X-SecretBroker-Token, and
com.secretbroker.*.
Release builds compile out every SECRETBROKER_DEV_* bypass. If you do not have a
Developer ID Application certificate, limit unsigned debug builds to isolated
tests with synthetic credentials:
swift build
BIN=.build/debug ./smoke.shThe smoke test creates its own temporary support directory and never uses the
production vault or Keychain item. If swift build fails because the local
Command Line Tools installation has conflicting module maps, ./build.sh
provides a direct swiftc fallback:
./build.sh
BIN=.build-manual ./smoke.shFixing or reinstalling the matching Xcode toolchain remains the preferred solution.
After install.sh starts the menu-bar app and its daemon, select the installed CLI:
SECRETBROKER_CLI="$HOME/Library/Application Support/SecretBroker/bin/secretbroker"
"$SECRETBROKER_CLI" statusThe installer also creates a secretbroker symlink when /usr/local/bin or
/opt/homebrew/bin is writable.
Add a secret. SecretBroker reads the value from standard input so it does not enter shell history:
"$SECRETBROKER_CLI" add \
--name github \
--scope api.github.com \
--env-name GH_TOKENSecretBroker presents one non-cookie credential kind: Secret. You do not
choose between a token and an API key. Configure environment-variable names for
exec delivery and, when needed, a custom header for proxy delivery. Without a
custom header, the proxy sends Authorization: Bearer <secret>.
For a long-lived value stored in AWS Secrets Manager, 1Password, or another external system, first open Loaders and add the code that reads that system. A Loader can use managed Bash, Zsh, Python, or Java source, a JAR, or a custom executable. Then choose Add → Connect an External Secret, select the Loader, enter the provider reference, and use Test Connection.
Loaders are reusable. Each External Secret stores an exact Loader ID and
revision, so two secrets can share one approved implementation without copying
code. A secret can use the Loader's default Parse command, skip parsing, or
override it. SecretBroker runs the Loader out of process and keeps the loaded
value only in daemon memory. For interpreter invocations such as bash loader.sh, python loader.py, or java -jar loader.jar, the daemon binds the
primary code file to the approval by content hash.
Alternatively, register an on-demand provider. Commands are parsed into argv and are not implicitly executed through a shell:
"$SECRETBROKER_CLI" provider add \
--name github-cli \
--scope api.github.com \
--env-name GH_TOKEN \
--sign-in "/opt/homebrew/bin/gh auth login" \
--credential "/opt/homebrew/bin/gh auth token"After approval, SecretBroker runs Credential first. When Parse is configured, SecretBroker pipes at most 64 KiB of Credential stdout to Parse and uses only the Parse output. If the pipeline fails and Sign-in commands exist, the app opens Terminal, runs those commands in order, and retries Credential → Parse.
The acquired secret lasts for one exec operation or one proxy grant. It is
never returned through MCP. Test Command runs the same linked flow without
saving the provider or displaying the value.
If the GitHub CLI is installed, run an authenticated API call. gh reads the secret from GH_TOKEN:
"$SECRETBROKER_CLI" exec \
--scope api.github.com \
-- gh api userImport a live browser session:
"$SECRETBROKER_CLI" import \
--scope .github.com \
--browser chrome \
--profile DefaultSecretBroker stores a reference to a live browser profile by default and reads current cookies for each approved request. Add --snapshot to freeze the imported cookies.
Each request identifies a scope, delivery mode, and optional account. After
policy checks and user approval, secretbrokerd issues a short-lived grant for
a matching credential.
In exec mode, the CLI or MCP server starts the approved process with secret
environment variables or a private cookie jar. In proxy mode, the credential
stays inside SecretBroker, which injects cookies, a Bearer value, or a custom
header for the approved host. Release, expiry, or panic revokes the grant and
removes temporary files.
| Scenario or authentication mechanism | Built-in support | Injection path or limitation |
|---|---|---|
| CLI tools and SDKs that read environment variables | Yes | exec / sb_exec sets SECRETBROKER_TOKEN and the configured names in the child process |
| Browser-cookie authentication | Yes | exec cookie jar, Playwright/Netscape/header state file, or reverse-proxy Cookie header |
| Bearer-token HTTP APIs | Yes | Reverse proxy adds Authorization: Bearer; exec provides the secret through environment variables |
| Custom API-key headers | Yes | Reverse proxy adds the configured header, such as X-API-Key; exec provides the value through environment variables |
| Ordinary HTTP or HTTPS calls | Yes | Use the scope-locked reverse proxy; non-local upstreams use HTTPS, redirects are not followed, and WebSockets are not supported |
| Git over HTTPS with a personal access token | Yes | secretbroker git supplies the token through GIT_ASKPASS and disables interactive credential helpers |
| Playwright-compatible browser automation | Partial | State files contain cookies but not localStorage, sessionStorage, or passwords |
| OAuth/device-login CLIs that can print a token | Yes, via command provider | Configure Sign-in commands plus a Credential command and optional Parse command; the recipe runs as the macOS user and is not sandboxed |
| AWS Secrets Manager, 1Password, or another remote secret store | Yes, via External Secret | Select an approved reusable Loader and choose default, no, or custom parsing; SecretBroker caches the result only in daemon memory |
| Static OAuth or JWT access tokens | Yes | Store the value locally or load it as an External Secret; SecretBroker does not itself implement OAuth or token refresh protocols |
| Username/password, form login, HTTP Basic, Digest, or NTLM | No built-in adapter | SecretBroker has no structured username/password credential or challenge-response implementation |
| SSH keys, client certificates, mTLS, passkeys, WebAuthn, or TOTP | No built-in adapter | SecretBroker does not manage these keys, certificates, authenticators, or interactive ceremonies |
| Signed or multi-value schemes such as AWS SigV4 | No built-in adapter | SecretBroker does not assemble multi-field credentials or sign requests |
A trusted child process can implement other protocols with an injected secret, but it can also read and expose that plaintext. SecretBroker does not rewrite arbitrary command arguments, answer interactive password prompts, or place credentials on the command line.
In secretbroker exec ... -- curl ..., -- only separates SecretBroker options
from curl argv. Cookie credentials add a private cookie jar automatically.
Secret credentials enter environment variables; curl does not read them or
receive an automatic Authorization header. Use grant for broker-managed HTTP
header injection.
SecretBroker sits behind an MCP host. The agent chooses a scope and operation; secretbroker-mcp translates that tool call into a request to the local broker and returns only the operation result or a short-lived capability.
Install SecretBroker first, then register the installed MCP binary with your MCP host.
codex mcp add secretbroker -- \
"$HOME/Library/Application Support/SecretBroker/bin/secretbroker-mcp"
codex mcp listStart a new Codex session after registration. Run /mcp in the session to confirm that the SecretBroker tools are available.
claude mcp add -s user secretbroker -- \
"$HOME/Library/Application Support/SecretBroker/bin/secretbroker-mcp"
claude mcp listStart a new Claude Code session after registration.
For either host, the server provides these tools:
| Tool | Purpose |
|---|---|
sb_list_scopes |
List available credential scopes without returning secret values |
sb_exec |
Run a command with injected credentials and return captured output |
sb_get_state_file |
Create a short-lived browser or cookie state file |
sb_grant |
Create a short-lived, scope-locked reverse-proxy grant |
sb_request_scope |
Ask the user to import a browser session |
The following flow shows how an AI agent uses sb_exec. Other MCP tools use the same scope, policy, and approval checks.
sequenceDiagram
autonumber
actor User
participant Agent as AI agent
participant Host as MCP host
participant MCP as secretbroker-mcp
participant Daemon as secretbrokerd
participant App as SecretBrokerApp
participant Source as Vault or browser
participant Child as Approved command
participant Service as Target service
Agent->>Host: Call sb_list_scopes
Host->>MCP: MCP tools/call
MCP->>Daemon: List available scopes
Daemon-->>MCP: Scope metadata only
MCP-->>Host: Tool result
Host-->>Agent: Available scopes
Agent->>Host: Call sb_exec with scope and command
Host->>MCP: MCP tools/call
MCP->>Daemon: Request a scoped exec grant
Daemon->>Source: Resolve the matching credential
Source-->>Daemon: Selected credential
opt No exact reusable approval applies
Daemon->>App: Send the scope and full command
App->>User: Request approval and authentication
User-->>App: Approve or deny
App-->>Daemon: Return the decision
end
alt Request is denied or times out
Daemon-->>MCP: Structured error
MCP-->>Host: Failed tool result
Host-->>Agent: Explain the failure
else Request is approved
Daemon-->>MCP: Short-lived grant and injection material
MCP->>Child: Start the trusted command with injected credentials
Child->>Service: Send an authenticated request
Service-->>Child: Return the response
Child-->>MCP: Return stdout, stderr, and exit code
MCP->>Daemon: Release the grant
MCP-->>Host: Return a bounded, exact-value-redacted result
Host-->>Agent: Return the tool result
end
For browser automation, the agent can call sb_get_state_file and receive a short-lived state-file path. For repeated requests to one host, it can call sb_grant and receive a scope-locked proxy endpoint and token. Both outputs are sensitive capabilities and remain subject to user approval, expiry, revocation, and audit logging.
If the selected scope uses an External Secret or command provider, sb_exec
and sb_grant wait for the load automatically. An External Secret normally
loads in the background. If its loader returns exit code 20, or an on-demand
provider needs Sign-in commands, the app may open Terminal. The agent continues
the original tool call and receives only the operation result or proxy capability.
The MCP server redacts exact injected secret and cookie values from captured command output. This is defense in depth, not data-loss prevention: transformed, encoded, or indirectly exfiltrated values may evade redaction.
secretbroker status
secretbroker scopes
secretbroker list
secretbroker audit --since 1h
secretbroker audit --verify
secretbroker panicRun secretbroker help for the complete command list. See docs/guide.md for installation, policy, troubleshooting, and uninstall instructions.
SecretBroker runs as a local broker between automation clients and credential sources. Clients request an operation for a scope; they do not query the vault directly.
flowchart LR
User["User"] -->|"reviews request and authenticates"| App["SecretBrokerApp<br/>approval and credential management"]
subgraph Clients["Automation clients"]
CLI["secretbroker CLI"]
MCP["secretbroker-mcp"]
end
CLI -->|"JSONL over owner-only Unix socket"| Daemon["secretbrokerd<br/>scope, policy, and grant enforcement"]
MCP -->|"JSONL over owner-only Unix socket"| Daemon
App -->|"management requests and decisions"| Daemon
Daemon -->|"full command or operation summary"| App
subgraph Sources["Credential sources"]
Vault[("Encrypted vault<br/>key protected by macOS Keychain")]
External[("External secret manager<br/>approved reusable Loader")]
Browser[("Chromium cookie database<br/>read-only live import")]
end
Vault -->|"selected credential"| Daemon
External -->|"plaintext cached only in daemon memory"| Daemon
Browser -->|"fresh scoped cookies"| Daemon
Daemon --> Audit[("Hash-chained audit log")]
subgraph Delivery["Approved delivery modes"]
Exec["exec<br/>inject into a child process"]
State["state<br/>write an owner-only temporary file"]
Proxy["proxy<br/>create a scope-locked loopback endpoint"]
end
Daemon -->|"short-lived grant"| Exec
Daemon -->|"short-lived grant"| State
Daemon -->|"short-lived grant"| Proxy
- The CLI or MCP server asks
secretbrokerdto use a credential for a specific host scope and operation. - The daemon validates the request, identifies the local peer, selects a matching credential, and resolves the applicable policy.
- Unless an exact reusable approval applies, the menu-bar app shows the scope, credential, mode, and complete command or operation summary. The user approves or denies the request and authenticates with Touch ID or the account password.
- After approval, the daemon issues a short-lived grant. The CLI or MCP server applies returned environment bindings and cookie material when it starts the approved child process; the daemon can instead write a temporary state file or expose a scope-locked loopback proxy.
- The daemon records the decision in the audit log. Release, expiry, or panic revokes the grant and removes temporary credential material.
The broker reduces accidental plaintext exposure, but it does not make an approved process trustworthy. An approved child process can read injected values, and a state-file path acts as a temporary bearer capability.
The source tree is organized into:
| Path | Responsibility |
|---|---|
Sources/SecretBrokerCore |
Shared protocol, models, scope parsing, and socket helpers |
Sources/SecretBrokerChrome |
Chromium cookie discovery and decryption |
Sources/SecretBrokerVault |
Keychain integration, biometry, encrypted local values, and encrypted External Secret definitions |
Sources/secretbrokerd |
Broker, approval flow, audit log, proxy, and Unix socket server |
Sources/secretbroker |
Command-line client |
Sources/secretbroker-mcp |
MCP server |
Sources/SecretBrokerApp |
Menu-bar approval and credential-management app |
Sources/SecretBrokerTestSupport |
Shared core checks used by XCTest and lightweight toolchains |
Tests/SecretBrokerTests |
Standard SwiftPM test target |
Build and run the checks:
swift build
swift test
swift run secretbroker-checkRun the headless integration tests against isolated temporary data:
./smoke.sh
./test-approver.sh
./test-mcp.sh
./test-proxy.sh
./test-hardening.sh
./test-request-scope.sh
./test-protocol.sh
./test-command-provider.sh
./test-external-secret.sh
./test-app-lifecycle.shSome integration tests set SECRETBROKER_DEV_APPROVE=1,
SECRETBROKER_DEV_FILEKEY=1, and SECRETBROKER_DEV_ALLOW_UNSIGNED=1. These switches
work only in debug builds. Never use them with production credentials.
For maintainers, release.sh builds a versioned archive, applies hardened-runtime Developer ID signatures, submits it to Apple's notary service, saves the notarization log, staples the app ticket, verifies signatures, and writes a SHA-256 checksum. It refuses to create a release without both signing and notarization credentials.
xcrun notarytool store-credentials secretbroker-notary
SECRETBROKER_SIGNING_IDENTITY="Developer ID Application: Example (TEAMID)" \
SECRETBROKER_NOTARY_PROFILE=secretbroker-notary \
./release.sh 0.1.0The tag-based release workflow requires the Apple secrets documented in CONTRIBUTING.md. Never distribute the ad-hoc signed output from package-app.sh as a publisher-verified build.
See CONTRIBUTING.md for the development workflow, test expectations, and commit rules.
SecretBroker reduces accidental secret exposure; it cannot make an untrusted local process safe.
- Approval authorizes an operation, but
execdoes not sandbox the approved child process. - A state-file path is a capability. Any same-user process that can read the file before expiry can read its contents.
- A command provider runs approved executables with the macOS user's permissions. Configure only commands you trust.
- An External Loader is standing, user-approved code. It runs as the macOS user at startup, on demand, or on a configured interval. SecretBroker does not sandbox it or restrict its network and filesystem access.
- Updating a shared Loader requires one fresh approval that lists every affected External Secret. The daemon clears their caches, revokes their grants, and advances all bindings in one encrypted-registry transaction.
- External Secret definitions are encrypted at rest. Loaded values live only in daemon memory and are cleared on panic, deletion, configuration change, or app exit.
- Command-provider registration and use require a current, provider-capable SecretBroker app so the full recipe can be reviewed; there is no generic headless approval fallback.
- The installed daemon is app-owned. Opening SecretBroker starts it; quitting or crashing the app stops it, cancels active acquisitions, clears proxy grants, and shreds temporary material. CLI and MCP calls are unavailable while the app is closed.
- A Terminal launcher contains a short-lived provider-session capability and non-secret broker routing paths, but no recipe or credential. It is owner-only and removed on completion, cancellation, timeout, panic, or restart.
- Client labels are self-reported and must not be treated as authentication.
- The audit chain detects many edits within the retained log, but it does not prevent deletion or replacement by a process with the same filesystem access.
SECRETBROKER_DEV_APPROVE,SECRETBROKER_DEV_FILEKEY, andSECRETBROKER_DEV_ALLOW_UNSIGNEDweaken debug builds. Release builds compile them out.SECRETBROKER_ALLOW_RAW=1enables explicit plaintext CLI output after signed caller verification and fresh user authentication. Leave it unset for routine use.
Report vulnerabilities according to SECURITY.md.
- Installation and usage guide
- Use cases
- UX and approval behavior
- Interface contract
- Skill and tool authoring
- Claude instructions snippet
- Independent security review guide
- Changelog
Contributions are welcome. Review CONTRIBUTING.md and discuss security-sensitive design changes before implementing them.
SecretBroker is licensed under the Apache License 2.0.