-
Notifications
You must be signed in to change notification settings - Fork 1
RFC: Securely using Claude Code CLI with 1Password secrets #135
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
lyind
wants to merge
4
commits into
main
Choose a base branch
from
claude-code-1password-secrets
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,236 @@ | ||
| --- | ||
| creation_date: 2025-02-04 | ||
| issues: [] | ||
| owners: | ||
| - https://github.com/orgs/giantswarm/teams/team-up | ||
| state: review | ||
| summary: A guide for securely using Claude Code CLI and IDE plugins on Linux by storing secrets in 1Password and running the tool in a sandboxed environment using bubblewrap. | ||
| --- | ||
|
|
||
| # Securely Using Claude Code CLI with 1Password Secrets on Linux | ||
|
|
||
| ## Problem Statement | ||
|
|
||
| Claude Code CLI (and IDE plugins) can execute arbitrary commands, including those that require authentication tokens (e.g., `gh`, `devctl`, `opsctl`). Storing these secrets as plain environment variables or in dotfiles poses security risks: | ||
|
|
||
| 1. **Secret exposure**: Claude Code could accidentally leak secrets in logs, error messages, or when suggesting commands. | ||
| 2. **Overly broad access**: The tool might access files or directories outside the intended project scope. | ||
| 3. **Credential persistence**: Secrets stored in shell profiles or environment files remain accessible even when not needed. | ||
|
|
||
| ## Proposed Solution | ||
|
|
||
| Use a combination of: | ||
|
|
||
| 1. **1Password service accounts** with dedicated, limited-scope vaults for AI tool access | ||
| 2. **1Password CLI (`op`)** for just-in-time secret injection via `op://` URIs | ||
| 3. **Bubblewrap (`bwrap`)** sandboxing to restrict filesystem access and isolate the Claude Code process | ||
|
|
||
| This approach ensures secrets are never directly exposed to Claude Code while still allowing authenticated commands to work seamlessly. | ||
|
|
||
| ## Implementation | ||
|
|
||
| ### Step 1: Create a Dedicated 1Password Vault | ||
|
|
||
| 1. Log into [1Password Web](https://1password.com) and create a new vault named `ai-$NAME` (e.g., `ai-jonas`). | ||
| 2. Add the secrets Claude Code will need (e.g., `OPSCTL_GITHUB_TOKEN`) to this vault. | ||
|
|
||
| ### Step 2: Create a 1Password Service Account | ||
|
|
||
| 1. Create a 1Password service account named `ai-$NAME` with access **only** to the `ai-$NAME` vault. | ||
| 2. Store the service account token in your personal `Employee` vault, naming it `service-account-token-ai-$NAME`. | ||
|
|
||
| This ensures the service account has minimal permissions鈥攐nly accessing secrets explicitly placed in the AI vault. | ||
|
|
||
| ### Step 3: Configure Claude to run in Sandbox | ||
|
|
||
| #### OPTION A) External Sandbox via Launcher Script | ||
|
|
||
| Create a script (e.g., `~/bin/run-claude.sh`) that: | ||
|
|
||
| - Retrieves the service account token from 1Password | ||
| - Launches Claude Code inside a bubblewrap sandbox | ||
| - Passes secrets as `op://` URIs (not raw values) | ||
|
|
||
| ```sh | ||
| #!/usr/bin/env sh | ||
| set -e | ||
| PROJECT_DIR="$(realpath .)" | ||
| if [ -d "$1" ] | ||
| then | ||
| PROJECT_DIR="$(realpath "$1")" | ||
| fi | ||
|
|
||
| if [ "$PROJECT_DIR" = "$(realpath "$HOME")" -o "$PROJECT_DIR" = "$(realpath /)" ] | ||
| then | ||
| echo "Error: don't give claude access to high-level directories! Run it somewhere else." >&2 | ||
| exit 1 | ||
| fi | ||
|
|
||
| # GH_TOKEN and similar below contain only paths into the 1Password vault assigned to the service account | ||
| vault=ai-jonas | ||
| account_token="$(op item get --vault 'Employee' "service-account-token-$vault" --reveal --fields credential)" | ||
|
|
||
| temp_passwd="$(mktemp)" | ||
| trap 'rm -f "$temp_passwd"' EXIT | ||
| getent passwd "$(id -u)" >> "$temp_passwd" | ||
|
|
||
| # Ensure bind directories exist | ||
| for dir in .m2 .gradle go .npm .java .cache/pip .cache/helm .cache/go .cache/go-build .local/share/claude; do | ||
| mkdir -p "$HOME/$dir" | ||
| done | ||
|
|
||
| exec bwrap \ | ||
| --tmpfs /tmp \ | ||
| --unshare-user-try \ | ||
| --uid $(id -u) \ | ||
| --gid $(id -g) \ | ||
| --ro-bind /usr /usr \ | ||
| --ro-bind /lib /lib \ | ||
| --ro-bind /lib64 /lib64 \ | ||
| --ro-bind /bin /bin \ | ||
| --ro-bind /etc/resolv.conf /etc/resolv.conf \ | ||
| --ro-bind /etc/hosts /etc/hosts \ | ||
| --ro-bind /etc/ssl /etc/ssl \ | ||
| --ro-bind /etc/alternatives /etc/alternatives \ | ||
| --ro-bind "$temp_passwd" /etc/passwd \ | ||
| --ro-bind /etc/group /etc/group \ | ||
| --ro-bind "$HOME/.1password" "$HOME/.1password" \ | ||
| --ro-bind "$HOME/.nvm" "$HOME/.nvm" \ | ||
| --ro-bind "$HOME/.local" "$HOME/.local" \ | ||
| --ro-bind "$HOME/.ssh" "$HOME/.ssh" \ | ||
| --ro-bind "$HOME/.config/gh" "$HOME/.config/gh" \ | ||
| --ro-bind "$HOME/.kube" "$HOME/.kube" \ | ||
| --ro-bind "/opt" "/opt" \ | ||
| --bind "$HOME/.config/gws" "$HOME/.config/gws" \ | ||
| --dir "$HOME/bin" \ | ||
| --dir "$XDG_RUNTIME_DIR" \ | ||
| --ro-bind "$HOME/bin" "$HOME/bin" \ | ||
| --bind "$HOME/.gitconfig" "$HOME/.gitconfig" \ | ||
| --bind "$PROJECT_DIR" "$PROJECT_DIR" \ | ||
| --bind "$HOME/.claude" "$HOME/.claude" \ | ||
| --bind "$HOME/.claude.json" "$HOME/.claude.json" \ | ||
| --ro-bind "$HOME/.agents" "$HOME/.agents" \ | ||
| --bind "$HOME/.local/share/claude" "$HOME/.local/share/claude" \ | ||
| --bind "$HOME/.m2" "$HOME/.m2" \ | ||
| --bind "$HOME/.gradle" "$HOME/.gradle" \ | ||
| --bind "$HOME/go" "$HOME/go" \ | ||
| --bind "$HOME/.npm" "$HOME/.npm" \ | ||
| --bind "$HOME/.java" "$HOME/.java" \ | ||
| --bind "$HOME/.cache/pip" "$HOME/.cache/pip" \ | ||
| --bind "$HOME/.cache/helm" "$HOME/.cache/helm" \ | ||
| --bind "$HOME/.cache/go" "$HOME/.cache/go" \ | ||
| --bind "$HOME/.cache/go-build" "$HOME/.cache/go-build" \ | ||
| --bind "$SSH_AUTH_SOCK" "$SSH_AUTH_SOCK" \ | ||
| --setenv "OP_SERVICE_ACCOUNT_TOKEN" "$account_token" \ | ||
| --setenv "GH_TOKEN" "op://$vault/OPSCTL_GITHUB_TOKEN/password" \ | ||
| --setenv "GITHUB_TOKEN" "op://$vault/OPSCTL_GITHUB_TOKEN/password" \ | ||
| --setenv "OPSCTL_GITHUB_TOKEN" "op://$vault/OPSCTL_GITHUB_TOKEN/password" \ | ||
| --setenv "SSH_AUTH_SOCK" "$SSH_AUTH_SOCK" \ | ||
| --setenv "SSH_ASKPASS" "$SSH_ASKPASS" \ | ||
|
lyind marked this conversation as resolved.
|
||
| --setenv "DO_NOT_TRACK" "true" \ | ||
| --setenv "CLAUDE_CODE_NO_FLICKER" "1" \ | ||
| --proc /proc \ | ||
| --dev /dev \ | ||
| --share-net \ | ||
| --unshare-pid \ | ||
| --die-with-parent \ | ||
| --setenv IS_SANDBOX 1 \ | ||
| --chdir "$PROJECT_DIR" \ | ||
| --ro-bind /dev/null "$PROJECT_DIR/.env" \ | ||
| --ro-bind /dev/null "$PROJECT_DIR/.env.local" \ | ||
| --ro-bind /dev/null "$PROJECT_DIR/.env.production" \ | ||
| "$(command -v claude)" | ||
| ``` | ||
|
|
||
| Make the script executable: | ||
|
|
||
| ```bash | ||
| chmod +x ~/bin/run-claude.sh | ||
| ``` | ||
|
|
||
| #### OPTION B) Trust and use Claude's builtin sandbox support | ||
|
|
||
| Install a sandbox provider (e.g. Bubblewrap mentioned above), start Claude Code (*without sandbox/normally*), enter `/sandbox` in Claude to perform initial configuration. | ||
|
lyind marked this conversation as resolved.
|
||
|
|
||
| ### Step 4: Configure Claude Code to Use `op run` | ||
|
lyind marked this conversation as resolved.
|
||
|
|
||
| Claude Code must be instructed to prefix authenticated commands with `op run --` so that 1Password injects the actual secret values at runtime. | ||
|
|
||
| Add the following to your project's `.claude/CLAUDE.md` or global Claude settings: | ||
|
|
||
| ```markdown | ||
| # 1Password Integration | ||
|
|
||
| When running commands that require authentication tokens (gh, devctl, opsctl, etc.), | ||
| prefix them with `op run --` to inject secrets from 1Password. | ||
|
|
||
| Example: | ||
| - Instead of: `gh pr list` | ||
| - Use: `op run -- gh pr list` | ||
|
|
||
| This allows secrets stored as `op://` URIs to be resolved at runtime. | ||
| ``` | ||
|
|
||
| Alternatively a Claude [Skill](https://code.claude.com/docs/en/skills) may be created, as prototyped [in this PR](https://github.com/giantswarm/claude-code/pull/20). | ||
|
|
||
| ### Step 5: (OPTIONAL, external sandbox only) IDE Plugin Configuration | ||
|
|
||
| Configure your IDE's Claude Code plugin to use the launcher script instead of calling `claude` directly: | ||
|
|
||
| - **VS Code**: Update the `claude-code.executablePath` setting to point to `~/bin/run-claude.sh` | ||
| - **JetBrains IDEs**: Configure the Claude Code plugin's executable path in Settings | ||
|
|
||
| ## Security Properties | ||
|
|
||
| This setup provides several security guarantees: | ||
|
|
||
| | Property | Mechanism | | ||
| |----------|-----------| | ||
| | **Secret isolation** | Secrets exist only as `op://` URIs; actual values are injected only when `op run` executes | | ||
| | **Minimal vault access** | Service account can only access the dedicated AI vault, not your Employee vault | | ||
| | **Filesystem sandboxing** | Bubblewrap restricts access to only necessary paths; project `.env` files are masked | | ||
| | **Process isolation** | `--unshare-pid` prevents the sandboxed process from seeing other processes | | ||
| | **Scope limitation** | Script refuses to run in `$HOME` or `/` directories | | ||
|
|
||
| ## Customization | ||
|
|
||
| ### Adding More Secrets | ||
|
|
||
| To add additional secrets for Claude Code to use: | ||
|
|
||
| 1. Add the secret to your `ai-$NAME` vault in 1Password | ||
| 2. Add a `--setenv` line to the bwrap command with the `op://` URI: | ||
| ```bash | ||
| --setenv "MY_NEW_TOKEN" "op://$vault/my-secret-name/password" \ | ||
| ``` | ||
|
|
||
| ### (OPTIONAL, external sandbox only) Adjusting Filesystem Access | ||
|
|
||
| Modify the `--ro-bind` (read-only) and `--bind` (read-write) lines to match your development environment. The current configuration supports: | ||
|
|
||
| - Node.js (`.nvm`, `.npm`) | ||
| - Java (`.m2`, `.gradle`, `.java`) | ||
| - Go (`go`, `.cache/go`, `.cache/go-build`) | ||
| - Python (`.cache/pip`) | ||
| - Kubernetes/Helm (`.cache/helm`) | ||
|
|
||
| ## Prerequisites | ||
|
|
||
| - **1Password CLI** (`op`): Install from [1Password CLI documentation](https://developer.1password.com/docs/cli/) | ||
| - **Bubblewrap** (`bwrap`): Install via your package manager (e.g., `apt install bubblewrap`) | ||
| - **Claude Code CLI**: Install from [Anthropic](https://docs.anthropic.com/en/docs/claude-code) | ||
|
|
||
| ## Open Questions | ||
|
|
||
| - Should we provide a centralized, team-managed vault instead of individual `ai-$NAME` vaults? | ||
| - How should this integrate with CI/CD pipelines that also use Claude Code? | ||
| - Could the `run-claude.sh` script be improved, e.g. to make it more compatible or protect the service account token better? | ||
| - Mac support? Use docker or another sandboxing app? | ||
| - Enterprise monitoring/logging? | ||
|
|
||
| ## References | ||
|
|
||
| - [1Password Service Accounts](https://developer.1password.com/docs/service-accounts/) | ||
| - [1Password Secret References (op:// URIs)](https://developer.1password.com/docs/cli/secret-references/) | ||
| - [Bubblewrap Documentation](https://github.com/containers/bubblewrap) | ||
| - [Claude Code Documentation](https://docs.anthropic.com/en/docs/claude-code) | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.