{"id":50735742,"url":"https://github.com/mneves75/cf-toolkit","last_synced_at":"2026-06-10T13:02:19.236Z","repository":{"id":362974805,"uuid":"1260492713","full_name":"mneves75/cf-toolkit","owner":"mneves75","description":"Multi-account Cloudflare Wrangler with two independent locks against wrong-account deploys (macOS, Bash).","archived":false,"fork":false,"pushed_at":"2026-06-06T20:09:57.000Z","size":31,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-06T21:08:35.270Z","etag":null,"topics":["bash","cli","cloudflare","cloudflare-workers","direnv","keychain","macos","multi-account","wrangler"],"latest_commit_sha":null,"homepage":"https://github.com/mneves75/cf-toolkit","language":"Shell","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/mneves75.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null}},"created_at":"2026-06-05T14:51:09.000Z","updated_at":"2026-06-06T20:09:51.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/mneves75/cf-toolkit","commit_stats":null,"previous_names":["mneves75/cf-toolkit"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/mneves75/cf-toolkit","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mneves75%2Fcf-toolkit","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mneves75%2Fcf-toolkit/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mneves75%2Fcf-toolkit/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mneves75%2Fcf-toolkit/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/mneves75","download_url":"https://codeload.github.com/mneves75/cf-toolkit/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mneves75%2Fcf-toolkit/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34153483,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-10T02:00:07.152Z","response_time":89,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["bash","cli","cloudflare","cloudflare-workers","direnv","keychain","macos","multi-account","wrangler"],"created_at":"2026-06-10T13:02:16.051Z","updated_at":"2026-06-10T13:02:19.227Z","avatar_url":"https://github.com/mneves75.png","language":"Shell","funding_links":[],"categories":[],"sub_categories":[],"readme":"# cf-toolkit\n\n[![CI](https://github.com/mneves75/cf-toolkit/actions/workflows/ci.yml/badge.svg)](https://github.com/mneves75/cf-toolkit/actions/workflows/ci.yml)\n[![License: Apache-2.0](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](LICENSE)\n![Platform: macOS](https://img.shields.io/badge/platform-macOS-lightgrey.svg)\n![Shell: Bash](https://img.shields.io/badge/shell-bash-4EAA25.svg)\n\nMulti-account Cloudflare Wrangler, without a wrapper around every command.\n\nThree small scripts (with an optional `cf-toolkit` umbrella command) let each project\ndeploy to its **own** Cloudflare account safely: a token scoped to that account is loaded\nautomatically when you `cd` in, the target account is pinned in the project's\n`wrangler.jsonc`, and a guard refuses to deploy if the two ever disagree.\n\n\u003e **Credits / prior art.** This toolkit was inspired by\n\u003e [**novincode/cfman**](https://github.com/novincode/cfman), a CLI overlay that stores\n\u003e per-account tokens and injects them via `cfman wrangler --account \u003cname\u003e …`.\n\u003e cf-toolkit keeps the same goal but takes a different approach — see\n\u003e [How it differs from cfman](#how-it-differs-from-cfman).\n\n---\n\n## The model: two independent locks\n\nDeploying to the wrong account is the failure this prevents, with two locks that are\nindependent on purpose — if one is misconfigured, the other still stops you:\n\n1. **Target lock** — `account_id` is pinned in each project's `wrangler.jsonc`\n   (non-secret; it appears in dashboard URLs). Wrangler targets exactly that account.\n2. **Credential lock** — a per-account API **token**, scoped in the Cloudflare dashboard\n   to *only that account*, is loaded from the macOS Keychain by a `.envrc`. A token for\n   account B physically cannot act on account A — the API rejects it.\n\n`cf-toolkit guard` makes the agreement between the two explicit and *local*: it asks the\nCloudflare API whether the loaded token can access the pinned `account_id`, and fails\nbefore `wrangler deploy` ever runs.\n\n## Scripts\n\n| Script | What it does |\n|--------|--------------|\n| `cf-register-account \u003clabel\u003e` | Reads an API token from a hidden prompt, validates it via the token-verify API (works with any valid token, even a minimal `workers_scripts:edit` one), stores it in the macOS login Keychain as `cloudflare-token-\u003clabel\u003e`. Labels must match `[A-Za-z0-9][A-Za-z0-9._-]{0,63}`. |\n| `cf-init-project \u003clabel\u003e \u003caccount-id\u003e [name]` | Run inside a project: pins a 32-hex `account_id` in `wrangler.jsonc` (or top-level `.toml`), writes/updates a secret-free cf-toolkit block in `.envrc`, runs `direnv allow`. Idempotent. |\n| `cf-guard` | Fail-fast pre-deploy check: confirms the loaded token can access the pinned account. Wire as `\"predeploy\": \"cf-toolkit guard\"` or run manually. |\n| `cf-toolkit \u003ccmd\u003e` | Umbrella command: `cf-toolkit register-account\\|init-project\\|guard …`, plus `version` and `help`. Maps to the three scripts above, which still work directly. |\n\n## Install\n\n### Homebrew (recommended)\n\n```bash\nbrew install mneves75/tap/cf-toolkit   # cf-toolkit + the three cf-* scripts onto PATH\nbrew install direnv                    # runtime dependency for the auto-load-on-cd flow\n```\n\nThen add the direnv hook once:\n\n```bash\necho 'eval \"$(direnv hook zsh)\"' \u003e\u003e ~/.zshrc \u0026\u0026 exec zsh\n```\n\nUpgrade later with `brew upgrade cf-toolkit`.\n\n### Manual (clone and run, no install)\n\n```bash\nbrew install direnv\ngit clone https://github.com/mneves75/cf-toolkit.git ~/cf-toolkit\necho 'export PATH=\"$HOME/cf-toolkit:$PATH\"' \u003e\u003e ~/.zshrc\necho 'eval \"$(direnv hook zsh)\"'            \u003e\u003e ~/.zshrc\nexec zsh\n```\n\n## Quick start\n\n```bash\n# 1) Once per Cloudflare account — create a scoped token, then register it.\n#    Dashboard → My Profile → API Tokens → \"Edit Cloudflare Workers\" template,\n#    and set Account Resources → Include → \u003conly this account\u003e.\ncf-toolkit register-account pessoal\n\n# 2) Once per project — from inside the project directory.\ncf-toolkit init-project pessoal \u003caccount-id\u003e\n\n# 3) Deploy as usual. No login, no --account flag.\nwrangler whoami         # sanity check: resolves to the right account\ncf-toolkit guard        # optional explicit check\nwrangler deploy\n```\n\n`cf-toolkit help` lists every subcommand. The underlying scripts (`cf-register-account`,\n`cf-init-project`, `cf-guard`) are the same code and still work directly if you prefer.\n\nSee [`docs/HOWTO.md`](docs/HOWTO.md) for the full walkthrough, CI/CD parity,\ntroubleshooting, and uninstall.\n\n## How it differs from cfman\n\n| | [cfman](https://github.com/novincode/cfman) | cf-toolkit |\n|---|---|---|\n| Token at rest | plaintext `~/.config/cfman/tokens.json` (chmod 600) | macOS Keychain (encrypted) |\n| Picking the account | `--account \u003cname\u003e` on every command | automatic on `cd` (direnv) |\n| Wrong-account protection | token only | token **+** pinned `account_id` **+** `cf-toolkit guard` |\n| Per-command overhead | wraps every call (`cfman wrangler …`) | none — plain `wrangler …` |\n| CI/CD | separate from local flow | identical (`CLOUDFLARE_API_TOKEN` env both sides) |\n| Dependency | a third-party binary in the credential path | stock `direnv`, `security`, `curl`, `node` (+ `wrangler` for deploys) |\n\nNeither is \"wrong\" — cfman is a single self-contained binary, which some prefer.\ncf-toolkit trades that for OS-native secret storage, zero per-command friction, and a\nsecond independent lock.\n\n## Security notes\n\n- `.envrc` contains **no secret** — only a marked cf-toolkit Keychain lookup block — so it\n  is safe to commit. Existing `.envrc` content is preserved.\n- The Keychain item is created with `-T /usr/bin/security` so the `security` CLI (and\n  thus direnv) can read it without a GUI prompt. This is convenient, but it means same-user\n  local processes that can execute `/usr/bin/security` can also request the token.\n- `account_id` is **not** a secret and is committed in `wrangler.jsonc`.\n- `.dev.vars`, `.dev.vars.*`, `.env`, and `.env.*` (which *can* hold secrets) are added to\n  `.gitignore` by `cf-init-project`; it also warns if matching files are already tracked.\n- Limitation: during `cf-register-account`, the final `security add-generic-password -w`\n  storage step receives the token as a process argument. The Cloudflare validation request\n  and guard checks do not put the token in `curl` arguments. On a single-user Mac this is\n  acceptable; otherwise use the Keychain Access GUI to store the item and omit this helper.\n- **The credential lock requires single-account tokens.** A token created with *Account\n  Resources → all accounts* can act on every account, so it passes `cf-guard` for any\n  pinned `account_id` — defeating the credential lock and leaving only the pinned\n  `account_id`. Always scope each token to exactly one account.\n\nTo report a vulnerability, see [SECURITY.md](SECURITY.md).\n\n## Contributing\n\nIssues and PRs welcome — see [CONTRIBUTING.md](CONTRIBUTING.md). The offline test suite\n(`bash tests/run-offline.sh`) needs no Cloudflare credentials, and CI runs it plus ShellCheck\non macOS for every push and pull request.\n\n## License\n\n[Apache-2.0](LICENSE) © 2026 Marcus Neves.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmneves75%2Fcf-toolkit","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmneves75%2Fcf-toolkit","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmneves75%2Fcf-toolkit/lists"}