I Built gc-vault, a CLI Tool for Operating Google Cloud Without Leaving Long-Term Credentials Locally

I Built gc-vault, a CLI Tool for Operating Google Cloud Without Leaving Long-Term Credentials Locally

Long-lived gcloud auth login tokens risk leaking, and letting agents like Claude Code use the CLI adds the risk of unintended changes. I built gc-vault, a thin CLI wrapper issuing short-lived tokens from 1Password.
2026.05.18

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.

https://github.com/zenn-dev/gc-vault

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

~/.zshrc (end of file)
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.

Share this article