I Built gc-vault, a CLI Tool for Operating Google Cloud Without Leaving Long-Term Credentials Locally
This page has been translated by machine translation. View original
Introduction
Recently, I wrote an article titled Replacing ADC with 1Password + Service Account Impersonation for Google Cloud authentication in local Terraform. For running Terraform, I switched to a method where run.sh retrieves a bootstrap SA key from 1Password and issues a short-lived impersonation token, so that no long-term credentials remain on the local disk.
However, Terraform is not the only way I interact with Google Cloud from my local machine. Recently, I also ask coding agents (such as Claude Code) to do things like "investigate the logs and analyze the costs."
When letting an agent use the CLI, in addition to the risk of credentials leaking, I also worry about the risk of it making unintended changes. It can easily happen that you ask it to investigate something and it ends up running a write command. I want to avoid handing over the same strong permissions that a human has, and I want to apply the principle of least privilege for each entity that uses the tools.
Writing a wrapper script like run.sh every time isn't practical, and I also wanted to avoid going back to gcloud auth login. So I built gc-vault, a generic thin wrapper CLI.
In this article, I introduce what this tool can do, why I built it, and how to use it.
What the tool does
When you write any command after gc-vault exec, it issues a short-lived impersonation token from 1Password behind the scenes and runs the command with that token provided.
gc-vault exec my-app-dev -- gcloud projects describe my-app-dev
gc-vault exec my-app-dev -- bq query 'SELECT count(*) FROM ...'
gc-vault exec my-app-dev -- terraform plan
gc-vault exec my-app-dev -- bin/rails console
The key point is that permanent credentials like those from gcloud auth login are written nowhere. The impersonation token exists only in memory and disappears when the process ends. Any tool that looks at CLOUDSDK_AUTH_ACCESS_TOKEN or ADC will work, so gcloud / bq / Terraform / applications using various client libraries are all supported as they are.
Why I built it
The refresh tokens left behind by gcloud auth login / gcloud auth application-default login have a practically unlimited lifetime (*), and if ~/.config/gcloud/ leaks, all projects including production could be operated for a long time without the owner noticing.
Google Cloud provides the building blocks such as --impersonate-service-account, but I couldn't find a thin wrapper that standardizes the flow of "store a long-term key in 1Password and issue a short-lived token only when needed", so I decided to build one myself.
Architecture
The design goal is narrowed down to a single point: "leave absolutely no long-term credentials on the local disk."
1Password
└── bootstrap-{user}@{project} の SA キー JSON
│ op read (1Password 認証)
▼
gc-vault exec <profile> -- <cmd>
1. 1Password から bootstrap SA キーを取得し一時ファイルへ
2. CLI 用: iamcredentials.generateAccessToken で短命トークン (1h) を取得
→ CLOUDSDK_AUTH_ACCESS_TOKEN にセット
3. SDK 用: impersonated_service_account 形式の ADC JSON を生成
→ GOOGLE_APPLICATION_CREDENTIALS にセット
(SDK 側がトークンを自動更新)
4. cmd を exec
5. 終了時に一時ファイル削除
The reason for providing two kinds of environment variables, one for CLIs and one for libraries, is that CLIs such as gcloud / bq read CLOUDSDK_AUTH_ACCESS_TOKEN (a static token string), while Google Cloud Client Libraries and the Terraform Provider read ADC (GOOGLE_APPLICATION_CREDENTIALS). For the latter, if you pass an ADC JSON in the impersonated_service_account format, the SDK refreshes the token automatically as needed, so long-running processes don't need to worry about token expiration.
Assumed SA configuration
In each Google Cloud project, create a bootstrap SA for each user and one or more target SAs.
| SA | Role | Permissions |
|---|---|---|
bootstrap-{user}@{project} |
Impersonation source (key is stored in 1Password) | Only roles/iam.serviceAccountTokenCreator on the target SA to be impersonated |
target-{role}-{user}@{project} |
Impersonation target (actually operates on resources) | Least privilege according to the role |
The bootstrap SA has no permissions other than impersonating the target, so even if the key leaks via 1Password, it can't do anything on its own. The damage is limited to the permission scope of the target being impersonated.
You can create multiple target SAs for each operating entity (role). For example, you can create target-user-{me}@{project} for your own operations and target-claude-{me}@{project} with only readonly permissions for Claude Code, and switch between them per profile. The naming convention can be freely adjusted to suit your operations.
Usage
Configuration file
Define profiles in ~/.config/gc-vault/config.toml. For each profile, you specify which 1Password item to start from and which target SA to impersonate.
For details, see the README and examples/config.toml.
gc-vault exec
Runs a command once with impersonated credentials.
$ gc-vault exec my-app-dev -- gcloud projects describe my-app-dev
$ gc-vault exec my-app-dev -- bq query 'SELECT ...'
$ gc-vault exec my-app-dev -- terraform plan
gc-vault shell
For consecutive operations, you can start a subshell with the impersonated credentials set. When you leave with exit, the environment variables disappear as well.
$ gc-vault shell my-app-dev
gc-vault: starting subshell with profile "my-app-dev" (exit to leave)
$ gcloud run services list
$ gcloud sql instances list
$ exit
Since the GCP_VAULT_ACTIVE_PROFILE environment variable is set during shell, I also recommend showing the profile name in your prompt to visualize "which environment you're in right now" (it needs to be placed at the end of your rc file, after the theme is loaded).
if [[ -n "$GCP_VAULT_ACTIVE_PROFILE" ]]; then
PROMPT="(gcp:$GCP_VAULT_ACTIVE_PROFILE) $PROMPT"
fi
gc-vault doctor
This command diagnoses the prerequisites of your local environment. It checks items together, such as whether gcloud / op are present, whether you are signed in to 1Password, whether the config file can be read, and whether old credentials remain in ~/.config/gcloud/.
$ gc-vault doctor
OK gcloud CLI found
OK 1Password CLI signed in: my.1password.com
OK config: /Users/alice/.config/gc-vault/config.toml (3 profile(s))
- my-app-dev
- my-app-stg
- my-app-prod
OK no bare gcloud credentials
Because gc-vault itself is designed not to touch gcloud's state, once you've migrated, remove the old ADC with the official steps below.
gcloud auth revoke --all
gcloud auth application-default revoke
After that, if any files remain, gc-vault doctor will warn you with WARN.
Using it with Claude Code
Claude Code's Bash tool has the sandbox enabled by default, and since this restricts things like macOS Unix domain sockets, the communication path from the op CLI to the 1Password desktop app is blocked, so calling gc-vault exec as is fails.
There is also the option of permanently allowing the necessary paths in the sandbox settings, but communication between op and the 1Password app spans multiple IPC paths, and once allowed, access to the entirety of 1Password would be always open in the event of prompt injection, so I did not adopt it.
Instead, what I have adopted is an approach of adding dangerouslyDisableSandbox: true on each call. The sandbox is lifted, but a Bash approval prompt appears, so nothing proceeds without the user's approval. In addition, because Claude Code's Bash tool spawns each call as an independent process, 1Password treats each execution as a separate session and also requires 1Password authentication for each call. The current understanding is that lifting the sandbox and 1Password authentication together form a two-stage defense.
The repository includes a Skill definition for Claude Code, and if you place it in ~/.claude/skills/, Claude Code will automatically assemble the flow above (gc-vault exec with dangerouslyDisableSandbox: true) at the necessary timing.
mkdir -p ~/.claude/skills
ln -s "$(pwd)/.claude/skills/gc-vault" ~/.claude/skills/gc-vault
Alternative if the authentication prompts are annoying
If approving 1Password authentication every time is annoying, there is also a way to enter a subshell with gc-vault shell before launching Claude Code.
gc-vault shell my-app-dev # ここで 1Password 認証
claude # 借用クレデンシャルを継承して起動
In this case, since Claude Code inherits the environment variables, you can call gcloud directly without going through gc-vault exec, and 1Password authentication is needed only once at the start.
However, as a trade-off, during the session CLOUDSDK_AUTH_ACCESS_TOKEN and the ADC file are readable by the agent. If these are extracted through prompt injection, the permissions of the target SA and the remaining lifetime of the bootstrap key are handed over to the attacker. If the target SA is restricted to readonly, the scope of damage can be limited, but I think it's safer to avoid this approach for profiles with write permissions.
Conclusion
My motivation for building gc-vault was to extend the principle I wrote about in the Terraform article, "don't leave long-term secrets on disk, and use short-lived tokens for actual operations," to the Google Cloud ecosystem as a whole.
It is still pre-alpha and is currently at the stage of dogfooding within our team. If there is a good response, I'd like to move forward with distribution via Homebrew and support for platforms other than macOS.
I'd be happy if this is helpful to people facing similar challenges, and I would appreciate it if you could share your opinions via Issues or comments.