cloudpin

io.github.paureisPublished by GitHub account paureisv1.2.3Updated Nov 5, 2026

Read-only checks that cloud CLI commands run on the accounts a project pins in .cloudpin.yml.

Overview

AI-generated overview

Read-only MCP server that tells an assistant which cloud account each CLI is using and whether a given command would run on the account a project pins.

What you can do
  • Report the active account for az, aws, gcloud, vercel, gh, kubectl, helm, supabase and wrangler, what the project's .cloudpin.yml pins, which environment applies and which agent hooks are installed
  • Evaluate a proposed shell command line as an agent would: list each cloud CLI call, the account it would use and why, and return an allow, block or ask verdict with the reason and the fix
  • Nothing is executed, switched or edited by the server
When to use it
Useful when an assistant runs cloud CLI commands in a project that pins accounts, so it can check the target account before deploying, deleting or changing anything. Also handy for explaining why a command was blocked.
Requirements
Runs as a local stdio process, started with the cloudpin MCP command from the npm package cloudpin; Node.js 22 or later. The project needs a .cloudpin.yml file, and the relevant cloud CLIs must be installed and logged in for their accounts to be read.
Before you install
The server is advisory only: it does not enforce anything, so a hook or shell wrapper is still needed to actually stop commands. It reads account identity through each CLI's own identity commands, which contact the providers, and reads the kubeconfig, Wrangler .env files and AWS config for account details. It never runs or switches accounts, so switching and editing .cloudpin.yml remain the user's job.

Installation

In SourceWeft

  1. Open cloudpin in the dashboard and add it to a workspace.
  2. Enable the server for the chats that should use its tools.

Desktop only via STDIO. STDIO servers start a local process, so they need the SourceWeft desktop host.

Other MCP clients

Follow the launch instructions in the repository.

README

[Image] cloudpin

[Tests] [OpenSSF Scorecard] [OpenSSF Best Practices] [npm] [Node] [License: MIT]

Pin cloud accounts to a project, and stop any command (yours or your AI agent's) that would run on the wrong one.

az, aws, gcloud, vercel, gh, kubectl, helm, supabase and wrangler act on whichever account, cluster or project you used last. With a work account, a personal one and a client or two on the same machine, sooner or later something gets deployed to, or deleted from, the wrong place. Coding agents make it more likely: they run these commands for you and never stop to ask which account is active. cloudpin checks before the command runs.

[cloudpin blocking a Vercel deploy on the wrong team, then blocking an AI agent's gh command on the wrong account]

Install

sh
pnpm add --global cloudpin

On macOS and Linux, Homebrew works too, from paureis/homebrew-tap (it runs on Homebrew's own Node.js, installed if it isn't there yet):

sh
brew install paureis/tap/cloudpin

On Windows, Scoop works too, from paureis/scoop-bucket. It installs Scoop's own Node.js (nodejs-lts) if needed, and cloudpin always runs on that one. A global install (scoop install -g) needs nodejs-lts installed globally too. nodejs-lts puts its folders at the front of your user PATH, so in new shells node and npm are Scoop's (and npm install --global installs there), ahead of any other Node.js on your user PATH; one on the system PATH, where the nodejs.org installer puts it, still comes first. Uninstalling nodejs-lts breaks cloudpin until you run scoop install nodejs-lts again, and Scoop's no_junction setting isn't supported.

powershell
scoop bucket add paureis https://github.com/paureis/scoop-bucketscoop install paureis/cloudpin

Without Node.js at all, each release from 1.3.0 has a standalone cloudpin.exe for 64-bit Windows: unzip cloudpin-X.Y.Z-win-x64.zip from Releases into a folder on your PATH. It carries its own Node.js, so a Node.js security fix reaches it only in a new cloudpin. winget (winget install paureis.cloudpin) comes with 1.3.0 too.

npm, Yarn, Bun, or no install
sh
npm install --global cloudpinyarn global add cloudpinbun add --global cloudpin
# try it without installingnpx cloudpin --help

Requires Node.js 22 or later. Works on Windows, macOS and Linux. The .cloudpin.yml format is stable from 1.0 on (see Configuration), and feedback is very welcome.

Latest release: 1.2.3 (2026-11-05): two security fixes since 1.2.1, please upgrade. 1.2.1 (2026-10-29): agent hooks now find calls hidden by heredocs in $( ), run-time values, PowerShell syntax and wrappers such as xargs, sudo, npx or cmd /c, and gcloud checks the impersonated account; please upgrade. 1.2.0 (2026-10-29) was run it everywhere. A GitHub Action (paureis/cloudpin-action) checks the accounts before a deploy job, built on the new cloudpin check --only and --json; Homebrew and Scoop installs; the MCP server for every agent, listed in the official MCP Registry (docs/mcp.md); Codex and Copilot CLI hooks that PowerShell's execution policy can't stop on Windows; and fixes to which Vercel team and Wrangler account a command uses. See the CHANGELOG. 1.1 added the Claude Code plugin, and 1.0 made the .cloudpin.yml format stable.

Quick start

sh
cd your-projectcloudpin init                        # pin the accounts you are logged into nowcloudpin install-hook claude         # protect your AI agent (or codex, cursor, copilot, antigravity, gemini)echo 'eval "$(cloudpin shell-init bash)"' >> ~/.bashrc   # protect your terminal

Commit .cloudpin.yml so everyone on the project gets the same protection. cloudpin status shows each CLI's active account next to what is pinned. To see a block without risking anything, pin a wrong account in a scratch folder (edit an ID in .cloudpin.yml) and run a read-only command such as gh repo list.

To undo it all: cloudpin uninstall-hook claude, remove the shell-init line from your profile, and delete .cloudpin.yml. The only other things cloudpin writes are a short-lived identity cache in your user cache folder, .git/cloudpin-env if you ran cloudpin use, and *.cloudpin-backup copies next to agent settings it edited.

Usage

sh
# Write .cloudpin.yml from the accounts you are logged into nowcloudpin init
# ...or add them as one environment of the file (see Environments)cloudpin init --env production --protected
# Show the environments, or choose the one to use in this clonecloudpin use [<env> | --clear]
# Check every pinned account; exit 1 on a mismatch (handy in CI and scripts)cloudpin check
# Check only these clouds; each must be pinned, installed and logged in to the pinned# account, so a CLI missing on the runner fails, not skipped. --json prints an overall "result" (match,# mismatch, unpinned or error) and each cloud's status, reason and fix. Exit codes stay the same.cloudpin check --only azure,aws [--json]
# Switch each pinned CLI to its pinned account: shows the plan, asks once (--yes: don't ask), runs the CLIs' own switch# commands, then checks again; exit 1 unless everything matches. What it won't do (a login, supabase link, a Wrangler# config edit, an overriding variable) it names for you. --json: the plan, what ran and the check after.cloudpin switch [--yes] [--json] [--only <clouds>]
# See each CLI's active account, what this folder pins, and which agent hooks are installed (--json for tools)cloudpin status
# Why was a command blocked, or would it be? See the pin, the account it would use, and the verdict, without# running it. One quoted argument is read as a full line; --agent shows what an agent hook decides; --json for tools.cloudpin explain -- vercel deploy --prodcloudpin explain "cd ../web && vercel deploy"
# Something looks off? Check the whole setup and get the fix for each problem; exit 1 if anything fails.# --json shortens account IDs and hides your home folder, so it is safe to paste in a bug report.cloudpin doctor [--json]
# Run one command only if it would act on the pinned account; exit 3 if blockedcloudpin exec -- vercel deploy --prod
# Shell functions that route az, aws, gcloud, vercel, gh, kubectl, helm, supabase and wrangler through cloudpincloudpin shell-init bash|zsh|pwsh
# Add or remove the hook in an agent's settings (this project, or yours with --user)cloudpin install-hook <agent> [--user]cloudpin uninstall-hook <agent> [--user]

How it works

A .cloudpin.yml in your project lists the accounts it uses. cloudpin init writes it for you, with readable names as comments:

yaml
azure:  subscription: "3f2a0000-0000-0000-0000-000000000c91" # Acme Prod  tenant: "8b1d0000-0000-0000-0000-00000000044e"aws:  account: "123456789012"gcloud:  account: "[email protected]"  project: "acme-prod"vercel:  team: "team_x9KqZr" # acmegithub:  user: "acme-bot"kubernetes:  server: "https://acme-prod.hcp.westeurope.azmk8s.io:443" # acme-prod-admin  namespace: "payments"supabase:  project: "abcdefghijklmnopqrst" # Acme prod  org: "acme"cloudflare:  account: "0123456789abcdef0123456789abcdef" # Acme

Before a guarded command runs, cloudpin works out which account that command would use (asking the CLI, or for Kubernetes reading the kubeconfig), honouring the command's own flags and environment, and compares the answer with the file:

CLIWhat is pinnedWhat cloudpin takes into account
azsubscription ID, tenant ID--subscription, AZURE_CONFIG_DIR
awsaccount ID--profile, AWS_PROFILE, access key variables, SSO
gcloudaccount, project--account, --project, --configuration, --impersonate-service-account, --access-token-file, CLOUDSDK_*
vercel / vcteam ID--scope, --team, --token, VERCEL_TOKEN, VERCEL_ORG_ID, linked project (.vercel/project.json or a repository's .vercel/repo.json)
ghuser, host--hostname, GH_HOST, GH_TOKEN, GITHUB_TOKEN
supabaseproject ref, organisation (optional)--project-ref, SUPABASE_PROJECT_ID, the project supabase link saved, --workdir, SUPABASE_WORKDIR, --profile, SUPABASE_ACCESS_TOKEN
wrangler (Cloudflare)account IDaccount_id in wrangler.json, wrangler.jsonc or wrangler.toml (and its [env.NAME] with --env / CLOUDFLARE_ENV), --config, --cwd, CLOUDFLARE_ACCOUNT_ID, the project's account cache, the credentials' only account
kubectl, helmAPI server URL, namespace (optional)--kubeconfig, KUBECONFIG, --context, --cluster, --server, -n, -A, kuberc; helm's --kube-context, --kube-apiserver, HELM_*

For Kubernetes the server URL is pinned rather than the context name, because context names are personal: the same cluster is prod on one laptop and acme-prod-admin on another. init writes your context name as a comment, and pins the namespace only if your context sets one. With a namespace pinned, -A / --all-namespaces is a mismatch. Commands that never reach a cluster (kubectl config, kubectl kustomize, helm template, helm repo, ...) are never blocked, and standalone kustomize isn't guarded at all. cloudpin can't see a namespace: written inside a manifest you apply, so pin the namespace and keep manifests namespace-free if that matters to you.

For gcloud, an impersonated service account (the last one in a delegation chain) is the account compared with the pin; with an access token or token file the account can't be known, so an account pin gives "can't tell".

For Supabase the project ref is worked out the way the CLI does it, without a network call: --project-ref, then SUPABASE_PROJECT_ID, then the project supabase link saved in supabase/.temp/. A linked branch counts as its parent project. The organisation comes from supabase projects list. The local stack (start, stop, status, functions serve, and db reset, db diff or migration up without --linked) is never blocked. A --db-url pointing anywhere but your own machine is stopped, because cloudpin can't tell which project a URL belongs to.

For Cloudflare, cloudpin follows Wrangler's order: account_id in the config file Wrangler would find, then CLOUDFLARE_ACCOUNT_ID, then the account Wrangler cached, then the only account the login sees (wrangler whoami --json), with the auth profile the command uses (--profile or wrangler auth activate). Variables Wrangler loads from the project's .env files count too. If it sees several and none is set, Wrangler would ask or fail, so cloudpin stops the command, as for a profile it can't ask about. Local commands (dev, and d1, kv key or r2 object without --remote) are never blocked; dev with remote: true bindings reaches your account unchecked. Built to Wrangler's source and docs, not yet run against a real Cloudflare login; reports welcome.

  • Match: the command runs as if cloudpin wasn't there.
  • Mismatch: the command is stopped, and you're told which account is active and how to switch (or to run cloudpin switch).
  • Can't tell (an expired login, a broken token, no network): the command is stopped too, and cloudpin says so plainly, with the CLI's own reason and the command that shows what's wrong.

Logging in, logging out, switching accounts, --help and --version are never blocked (where a CLI would still run the command, as vercel deploy -v does, the command is checked). A folder without a .cloudpin.yml is never affected. Monorepos work: the nearest .cloudpin.yml above the current folder wins.

Environments

One file can pin staging and production separately. Mark an environment protected and, even on the right account, any command that could change something asks first:

yaml
environments:  staging:    vercel: { team: "team_staging" }    aws: { account: "111111111111" }  production:    protected: true    vercel: { team: "team_prod" }    aws: { account: "222222222222" }branches:          # optional: which environment a git branch uses  main: production  "release/*": production  "*": stagingread_only:         # optional: more commands that never need confirmation  azure: ["webapp log tail"]

Which environment applies, first match wins:

  1. CLOUDPIN_ENV=<name> in the environment.
  2. cloudpin use <name>: remembered for this clone, inside .git, so it is never committed.
  3. The branches mapping for the checked-out branch, or for CLOUDPIN_BRANCH=<name>: an exact name first, then the first matching pattern.
  4. The first environment listed.

An unknown name is an error, so the command is stopped rather than checked against the wrong pins. cloudpin status and every message say which environment applies and why.

On a protected environment, read-only commands run as usual. Everything else asks:

  • In a terminal: Continue? [y/N].
  • Without a terminal (CI, scripts): the command stops unless CLOUDPIN_CONFIRM=<name> is set.
  • Claude Code, Copilot CLI, Antigravity and Cursor ask you through the agent's own prompt. Codex and Gemini CLI have no working "ask" in their hooks, so the command is blocked and the agent is told to hand it to you.

The built-in read-only list is short on purpose (az ... list|show, aws <service> describe-*|list-*|get-* and s3 ls, gcloud ... list|describe, vercel ls|inspect|logs and <group> ls, gh <group> list|view|status, kubectl get|describe|logs|top|explain, helm list|status|history|get), so anything it doesn't know asks. read_only can add commands, matched by their leading words, but never remove any. A wrong account is still blocked outright, protected or not.

Upgrading from the flat format: nothing to do; a file without environments keeps working exactly as before. To split it, move the provider sections under environments: and a name, or start a fresh file with cloudpin init --env <name> and run it again per environment (after switching accounts).

Protect your terminal

Add one line to your shell profile. Removing it uninstalls.

sh
eval "$(cloudpin shell-init bash)"                          # ~/.bashrceval "$(cloudpin shell-init zsh)"                           # ~/.zshrc
powershell
cloudpin shell-init pwsh | Out-String | Invoke-Expression   # $PROFILE

If cloudpin is ever uninstalled, the functions fall back to the real CLI, so they never get in your way. To run one command anyway, on purpose: CLOUDPIN_SKIP=1 <command>.

The line also defines a cloudpin function, so cloudpin switch can set AWS_PROFILE in your shell (AWS has no "switch" command; the account follows the profile). It applies only an AWS_PROFILE=<name> line from a temporary file (never running anything in it). Without the line, cloudpin switch prints the export for you to run.

Aliases. An alias that calls the CLI itself, such as alias kubectl='kubectl --context dev', is still guarded. An alias that runs another program, such as alias kubectl=kubecolor or alias gh='command gh', bypasses cloudpin, so each new shell prints a warning naming it (bash 4+, zsh and PowerShell; PowerShell runs any alias before the wrapper). Put the cloudpin shell-init line after your aliases so it can see them.

Protect your AI agent

sh
cloudpin install-hook claude

The hook goes into the project's agent settings (--user for your personal settings). cloudpin shows the file before writing it, keeps a backup, and uninstall-hook removes only its own entry. When a command is blocked, the agent is told why and asked to check with you. Agents can't use CLOUDPIN_SKIP or CLOUDPIN_CONFIRM, can't pick an environment by setting CLOUDPIN_ENV in their own command, and need your OK to run cloudpin use or cloudpin switch (with --yes too; agents whose hooks can't ask are told to ask you to run it).

AgentSettings fileStatus
Claude Code.claude/settings.jsonTested inside the agent
Codex.codex/hooks.jsonTested inside the agent
Cursor.cursor/hooks.jsonTested inside the agent (Cursor CLI)
GitHub Copilot CLI.github/hooks/cloudpin.jsonTested inside the agent
Antigravity (CLI, IDE and 2.0).agents/hooks.jsonTested inside Antigravity CLI
Gemini CLI.gemini/settings.jsonBuilt to the documented hook format

Google replaced Gemini CLI with Antigravity CLI for free and consumer accounts on 2026-06-18; Gemini CLI still serves paid API keys and enterprise licenses, so both are supported.

cloudpin reads the whole command line the agent is about to run, so it also catches cd ../other && npx vercel deploy, bash -c "...", pipelines, $( ), xargs, find -exec, heredocs fed to a shell (bash <<EOF), commands held in a variable (CLI=vercel; $CLI deploy) and PowerShell assignments ($prs = gh pr list). Variables set earlier in the line count too (export AWS_PROFILE=other; aws ..., $env:AWS_PROFILE = 'other'), and when one that decides the account is set from something only known when it runs (GH_TOKEN=$(cat file) gh ...), the command is stopped with the reason. It can't see inside a script file the agent writes and runs later (bash deploy.sh).

On Windows, Codex and Copilot CLI run hooks through PowerShell, whose Restricted and AllSigned execution policies block npm's cloudpin.ps1. So for them, install-hook also writes a Windows command starting cloudpin through cmd.exe, in a Windows-only field (Codex 0.131.0 or later). A hook from before 1.2.0 doesn't have it: re-run cloudpin install-hook <agent> after upgrading (Codex runs the changed hook once you review it in /hooks). Gemini CLI has no such field, so its hook needs a policy that runs local scripts, such as RemoteSigned. cloudpin doctor warns about each of these, about a . or other relative entry in PATH, and about Claude Code older than 2.1.47.

Claude Code plugin

One install gives Claude Code the hook, four read-only tools it can call before acting, and a short skill telling it to check first and to stop and ask you when cloudpin blocks a command. cloudpin itself must be installed first:

sh
npm install --global cloudpinclaude plugin install cloudpin --marketplace paureis/cloudpin

The plugin replaces cloudpin install-hook claude: with the plugin enabled, install-hook claude declines, and cloudpin doctor warns if both are present (Claude Code would run both).

MCP server

cloudpin mcp is an MCP server (stdio) for any agent; the Claude Code plugin starts it. Its four tools, cloudpin_status, cloudpin_check, cloudpin_doctor and cloudpin_switch_plan, report the accounts in use, if a command would be allowed, why cloudpin may not guard, and what you'd run to switch. None runs or changes anything: an agent that could switch accounts could switch to the wrong one. docs/mcp.md adds it to Codex, Cursor, VS Code and more. It is advice, not enforcement: where the agent has a hook, install that too.

On the Claude Agent SDK? docs/agent-sdk.md shows how to guard it.

Configuration

VariableEffect
CLOUDPIN_SKIP=1Run one command without the check (terminal only; ignored for agents)
CLOUDPIN_ENV=<name>Use this environment (see Environments)
CLOUDPIN_BRANCH=<name>Use this branch for the branches mapping, as in CI; ignored for agents
CLOUDPIN_CONFIRM=<name>Confirm changing commands on this protected environment without a prompt (terminal and CI only; ignored for agents)
CLOUDPIN_NO_CACHE=1Always ask the CLI instead of using the identity cache
CLOUDPIN_CACHE_DIRWhere the identity cache (and the time of the last update check) lives
CLOUDPIN_NO_UPDATE_CHECK=1Never ask npm whether a newer cloudpin exists (NO_UPDATE_NOTIFIER works too)

Asking a CLI who it is takes 0.5 to 2 seconds, so cloudpin remembers a successful answer for up to five minutes. The cache is dropped the moment anything that decides the account changes: the command's flags, the relevant environment variables, or the CLI's own account files (which az account set, gh auth switch and vercel switch rewrite). cloudpin check always asks the CLI.

From 1.0 on, .cloudpin.yml only grows: a new version may add sections and keys, never change or remove them. A file that uses a name your cloudpin doesn't know is refused rather than half read, so a typo such as azrue: can't switch protection off. The message names your version and says to update in case the file was written for a newer one.

Exit codeMeaning
0OK
1Usage error, check found a problem, or a CLI is still off its pin after switch
3Command blocked, or not confirmed on a protected environment

Security and privacy

cloudpin only reads which account is active, using each CLI's own identity command (az account show, aws sts get-caller-identity, gcloud config list, vercel teams ls, gh api user). It never logs in, and switches accounts only when you run cloudpin switch (it shows the plan and asks first unless you pass --yes; an agent's attempt asks you through its hook). It never reads a credential file other than the kubeconfig (taking only the server and namespace from it), the .env files Wrangler loads (taking the account variables) and, for cloudpin switch, the AWS config file (only profile names, sso_account_id and role_arn; never the credentials file), and never prints or stores a token: error messages name a variable such as GH_TOKEN but never its value, and the cache stores salted hashes of anything sensitive. It runs CLIs directly, never through a shell. cloudpin has no telemetry and sends nothing about you or your accounts; the identity commands above talk only to their own provider, as they would if you ran them. Its one request of its own asks registry.npmjs.org for the latest cloudpin version, at most once a day and only when you run a cloudpin command in a terminal (never from the shell wrappers, exec or the agent hooks), to tell you when an update is out. Turn it off with CLOUDPIN_NO_UPDATE_CHECK=1 or NO_UPDATE_NOTIFIER=1; it is off in CI. See SECURITY.md to report a problem.

FAQ

Does it slow my commands down? Outside a pinned project, by the time it takes Node to start (about 0.1 s). Inside one, the first check costs one identity call, and later checks come from the cache.

What if a CLI isn't installed? A pinned CLI that isn't installed is skipped; the command would fail anyway. cloudpin check --only is the exception: it requires every cloud listed.

Can I use it in CI? Yes. On GitHub Actions, add paureis/cloudpin-action@v1 after the job's cloud logins. Elsewhere, run cloudpin check before deploying; it exits 1 if any pinned account doesn't match. Hosted runners have many cloud CLIs preinstalled, so name the clouds the job deploys to with cloudpin check --only azure,aws: each must then be installed and logged in to the pinned account. On a pull request or tag there is no branch; set CLOUDPIN_BRANCH, e.g. to GITHUB_HEAD_REF, so branches picks the right environment.

Why block when it can't tell? Because "probably the right account" is the situation cloudpin exists to prevent. The message tells you exactly why it couldn't tell.

Roadmap

Ideas and what's next are in ROADMAP.md. Suggestions are welcome in issues.

Development

Contributor commands
sh
git clone https://github.com/paureis/cloudpin.gitcd cloudpinnpm cinpm run buildnpm test
CommandDescription
npm run buildCompile to dist/
npm testRun the test suite
npm run typecheckType-check without emitting
node scripts/smoke.mjsCheck providers against the CLIs logged in on your machine (after a build)
node scripts/demo.mjsRegenerate assets/demo.svg (after a build)

The design decisions, and why they were made, are in DESIGN.md. See CONTRIBUTING.md before opening a pull request.

About

Made by Alvaro Reis (LinkedIn), with Claude Code as a pair programmer. Every behaviour is covered by tests, and every CLI integration is checked against the tool's own documentation and the real CLI. Why it exists: 5 ways your cloud CLI picks the wrong account without telling you.

MIT

Source: README.md at commit 48cdd0c

Tools

2

cloudpin_status

Which cloud accounts each CLI (az, aws, gcloud, vercel, gh, kubectl, helm, supabase, wrangler) is using here, what this project's .cloudpin.yml pins, which environment applies, and which agent hooks are installed. Call it before cloud work.

Parameterscwd

cloudpin_check

Would cloudpin allow this shell command, as an agent's? Returns each cloud CLI call in it, the account it would use and why, and the verdict (allow, block or ask) with the reason and the fix. Nothing is run.

Parameterscwdshellcommandrequired

Version history

4
  1. v1.2.3LatestNov 5, 2026
  2. v1.2.2Nov 5, 2026
  3. v1.2.1Oct 30, 2026
  4. v1.2.0Oct 29, 2026

Score

90% of the applicable checks met

How complete this listing is, computed from the registry entry, the endpoint and public sources. Not a rating of the server and not a statement that it is safe or working today: reachability is a dated check, and no installs or reviews go into the score.

  • Reached in the last 30 days25 pts
  • Auth declared10 pts

    Declare the credential headers in the registry entry, or keep the endpoint answering the probe so its auth can be classified.

  • Tools listedRequired15 pts
  • DescriptionRequired15 pts
  • Logo10 pts
  • Documentation15 pts
  • License10 pts