Version
alchemy@2.0.0-beta.67, Node 22.22.3, macOS.
Summary
A stored auth profile silently outranks CLOUDFLARE_ACCOUNT_ID / CLOUDFLARE_API_TOKEN. The deploy goes to the profile's account, reports Done: 4 succeeded, and prints a url for that account — with no hint that the environment asked for a different one. CI=1 alone does not override it, even though the CLI's own error message elsewhere advertises CI=1 as the way to "use environment-variable credentials".
The result is a cross-account misdeploy that looks like a clean success.
Setup
~/.alchemy/profiles.json:
{ "version": 0, "profiles": { "default": { "Cloudflare": {
"method": "oauth", "accountId": "<ACCOUNT_A>", "scopes": ["workers:write", "..."] } } } }
.env — a token scoped to a different account, which has no access to ACCOUNT_A:
CLOUDFLARE_API_TOKEN=<token valid only for ACCOUNT_B>
CLOUDFLARE_ACCOUNT_ID=<ACCOUNT_B>
Stack is a Cloudflare.Website.StaticSite with an explicit name.
Steps
export $(grep -E '^CLOUDFLARE_' .env | xargs)
rm -rf .alchemy/state/<stack>/dev # force a create, not an update
npx alchemy deploy --stage dev --yes ./alchemy.local.run.ts
Actual
[Worker] created
Done: 4 succeeded
{ url: 'https://landing.<ACCOUNT_A-subdomain>.workers.dev' }
The Worker is created in ACCOUNT_A. Verified directly against the API:
GET /accounts/<ACCOUNT_B>/workers/scripts -> []
GET /accounts/<ACCOUNT_A>/workers/scripts -> [..., "landing", ...]
Prefixing CI=1 changes nothing — still ACCOUNT_A.
Expected
One of:
- explicit
CLOUDFLARE_ACCOUNT_ID / CLOUDFLARE_API_TOKEN take precedence over a stored profile; or
- the deploy fails with a conflict error naming both accounts; or
- at minimum, a warning, and a
url output that reflects where the Worker actually went.
Silently preferring the profile is the one behaviour that cannot be caught by reading the output.
Workaround
Passing a profile name that has no stored credentials, together with CI=1:
CI=1 npx alchemy deploy --stage dev --yes --profile <unused-name> ./alchemy.local.run.ts
Either flag alone is not enough. After alchemy profile clear --profile default, the unused name gets persisted as {"method":"env"} and --profile <name> alone then works without CI=1.
Two smaller things noticed alongside:
ALCHEMY_PROFILE is only read from the real environment, not from .env, while CLOUDFLARE_* are read from .env. Understandable given ordering, but surprising and undocumented.
- The
url output stayed wrong on every misdirected run, naming the old subdomain rather than the account actually deployed to. Worth deriving it from the deploy result instead of from resolved config.
Version
alchemy@2.0.0-beta.67, Node 22.22.3, macOS.Summary
A stored auth profile silently outranks
CLOUDFLARE_ACCOUNT_ID/CLOUDFLARE_API_TOKEN. The deploy goes to the profile's account, reportsDone: 4 succeeded, and prints aurlfor that account — with no hint that the environment asked for a different one.CI=1alone does not override it, even though the CLI's own error message elsewhere advertisesCI=1as the way to "use environment-variable credentials".The result is a cross-account misdeploy that looks like a clean success.
Setup
~/.alchemy/profiles.json:{ "version": 0, "profiles": { "default": { "Cloudflare": { "method": "oauth", "accountId": "<ACCOUNT_A>", "scopes": ["workers:write", "..."] } } } }.env— a token scoped to a different account, which has no access toACCOUNT_A:Stack is a
Cloudflare.Website.StaticSitewith an explicitname.Steps
Actual
The Worker is created in ACCOUNT_A. Verified directly against the API:
Prefixing
CI=1changes nothing — still ACCOUNT_A.Expected
One of:
CLOUDFLARE_ACCOUNT_ID/CLOUDFLARE_API_TOKENtake precedence over a stored profile; orurloutput that reflects where the Worker actually went.Silently preferring the profile is the one behaviour that cannot be caught by reading the output.
Workaround
Passing a profile name that has no stored credentials, together with
CI=1:Either flag alone is not enough. After
alchemy profile clear --profile default, the unused name gets persisted as{"method":"env"}and--profile <name>alone then works withoutCI=1.Two smaller things noticed alongside:
ALCHEMY_PROFILEis only read from the real environment, not from.env, whileCLOUDFLARE_*are read from.env. Understandable given ordering, but surprising and undocumented.urloutput stayed wrong on every misdirected run, naming the old subdomain rather than the account actually deployed to. Worth deriving it from the deploy result instead of from resolved config.