Skip to content
Skip to main content
Two industrial toggle switches on an emerald green steel panel, one under a closed safety cover and one with the cover flipped open, a metaphor for Supabase CLI v3 asking for confirmation before hosted changes
9 min readBy Carlos Aragon

Supabase CLI v3 Migration: What Broke and How to Fix It

Supabase CLI v3 changes two defaults that break most CI pipelines: database commands like db push now run against your local stack unless you add --remote, and anything that changes a hosted project fails with HostedMutationConsentRequiredError unless you pass --yes. It became npm's lateston November 2 and Homebrew's default a day later. This morning I installed 3.0.0 next to 2.122.1 and ran both against one of my own hosted projects. One result surprised me enough that I'm leading with it.

Why Doesn't supabase db push Reach My Project Anymore?

Because in v3, "no target" means local. That covers db push, db pull, db dump, migration list, migration repair, gen types, inspect db and the storage commands, and it applies even when the folder is linked to a hosted project.

Here's what I didn't expect. My Mac mini already had a local Supabase stack running for a different app, on the default port. I created a fresh folder, linked it to my hosted project, and ran a bare migration list:

$ supabase migration list          # v3.0.0, checkout linked to my hosted project
Target: local stack (127.0.0.1:54322) · default; pass --remote for my-project (<ref>)

   Local | Local DB         | Time (UTC)
  -------|------------------|---------------------
   ` `   | `20260930000000` | `2026-09-30 00:00:00`
   ` `   | `20260930000100` | `2026-09-30 00:01:00`
   ... 40 more rows from a completely different project

The CLI connected to whatever Postgres answered on 127.0.0.1:54322 and showed me another project's 42 migrations as if they were mine. A bare db push in the same folder went to that database too (it stopped on a migration history mismatch, so nothing changed). On a laptop with two Supabase projects, or a CI job that runs supabase start for tests, the same command can succeed against the wrong database and print a green check.

The fix is one flag, but the useful habit is reading the new Target: line. v3 prints it on stderr for every command that resolves a target, and in a linked checkout it even says pass --remote for my-project. With --remote the line becomes Target: project my-project (<ref>) · productionand the dry run reported "Remote database is up to date."

v2.122.1 does warn you about this, but only in a linked checkout:

$ supabase db push --dry-run        # v2.122.1, linked checkout
Warning: supabase db push will target the local database by default in the
next major version. Pass --linked to keep targeting the linked project.

In an unlinked folder using --project-ref, the same v2 commands printed no warning at all. So if your CI never runs supabase link, the "upgrade to 2.122 and fix the warnings" step in the official migration guide will show you nothing.

How Do I Fix HostedMutationConsentRequiredError in CI?

Add --yes. Every command that changes a hosted project now asks first, and the default answer is No. When the CLI can't ask, it refuses. I deployed a throwaway Edge Function to my project to see exactly how:

$ supabase functions deploy zz-probe --remote=<ref>      # stdout not a TTY
Target: project my-project (<ref>) · production
Confirmation required: Deploy 1 Function (target: project my-project (<ref>)
· production). This run can't ask because stdout is not a terminal.
Nothing was changed.
Re-run with --yes to confirm.
# exit 1

$ supabase functions deploy zz-probe --remote=<ref> --yes
Confirmed by --yes: Deploy 1 Function on project my-project (<ref>) · production
Deployed Functions on project <ref>: zz-probe
# exit 0

"Nothing was changed" held up: the function list was still empty after every refused run. A few details from the tests that the error message doesn't spell out:

  • echo y | is dead. Piped input got the exact same refusal. If your scripts confirm prompts that way, they all fail.
  • -o jsoncounts as a run that can't ask, even in a terminal. The reason changes to "it writes machine-readable -o json output."
  • Pressing Enter declines. In a real terminal the prompt reads Deploy 1 Function on project ...? ○ Yes / ● No, and Enter returned context canceled.
  • secrets unset with no names now fails with "No secret names given. Nothing was unset." You need --all to clear everything. Good change.

For contrast, v2.122.1 in the same non-terminal run deployed the function and then deleted it, no prompt, no flag, exit 0 both times. That's the behavior every CI pipeline was quietly relying on.

Does SUPABASE_YES Work Instead of --yes?

Only from the shell, and only if no project env file mentions it. I tried three placements:

# SUPABASE_YES=1 in supabase/.env
Re-run with --yes to confirm. SUPABASE_YES in supabase/.env does not
confirm hosted changes; pass --yes.

# SUPABASE_YES=1 exported in the shell, AND a .env in the cwd that defines it
Re-run with --yes to confirm. SUPABASE_YES in .env does not confirm
hosted changes; pass --yes.

# SUPABASE_YES=1 exported in the shell, no .env
Confirmed by SUPABASE_YES: Deploy 1 Function on project my-project ...

The second case is the trap. If a .env, .env.local or .env.<NODE_ENV> in the working directory defines SUPABASE_YES at all, the CLI can't tell your exported value from the file's, so it ignores both. I'd skip the variable and put --yes on each command. It's visible in the diff, and a reviewer can see exactly which steps are allowed to touch production.

What Happens When Claude Code or Cursor Runs the CLI?

v3 has an "agent mode" that never prompts, even inside a terminal. I ran the same deploy in a real pseudo-terminal with different environment variables set. CLAUDECODE=1, CURSOR_AGENT=1, CODEX_SANDBOX, GEMINI_CLI=1 and AI_AGENT=1 each switched it on. OPENCODE=1didn't (the binary looks for OPENCODE_CLIENT). In agent mode the output also flips to a JSON envelope:

{"target":{"kind":"linked","project_ref":"<ref>","project_name":"my-project",
 "environment":"production"},"_tag":"Error","error":{
 "code":"HostedMutationConsentRequiredError",
 "message":"Confirmation required: Deploy 1 Function (...). This run can't ask
  because it is running in agent mode. Nothing was changed.",
 "suggestion":"This is an agent run: ask the user before re-running with --yes."}}

I like this one. That suggestion field is written for the model, and it says the right thing: ask the human. Read-only commands like gen types typescript --remote=<ref> still worked normally (805 lines of types). If you let an agent run migrations, as I do in some Claude Code GitHub Actions jobs, the job needs an explicit --yesin the command you hand it, or a human approval step before it. I'd pick the approval step.

Which Errors Show Up First After Upgrading?

For older projects, it's the config file. Any config.toml generated before v2.108.0 has an [inbucket] section, and v3 won't load it, so status, db push and migration list all died on the same line. Removed commands fail the same way, with exit 1 and the replacement:

$ supabase db remote commit
supabase db remote commit was removed.
Use `supabase db pull --remote` instead.

$ supabase gen keys
supabase gen keys was removed.
Use `supabase projects api-keys --remote=<ref>` to read a project's API keys.

$ supabase status                  # config.toml still has [inbucket]
[inbucket] in supabase/config.toml is no longer supported. Use [local_smtp] instead.

Rename [inbucket] to [local_smtp] and keep the keys. v2.108.0 and later already read the new name, so you can ship that rename today without waiting for the rest of the upgrade.

One more parsing change: --remote takes its value with =. In my linked folder, migration list --remote zz failed with Branch "zz" not found ... refs are exactly 20 lowercase letters, because it checked the word against the project's branches. In an unlinked folder, --remote <ref>with a space happened to work. Don't depend on that.

The old flags (--linked, --project-ref, --db-url) still work in v3 with a deprecation warning, and they're gone in v4. And if a security tool allowlists binaries: the v2 npm package shipped two (supabase at 89 MB plus supabase-go at 39 MB), v3 ships one 115 MB supabase.

The v2 to v3 Command Cheat Sheet

v2v3
supabase db pushsupabase db push --remote=$REF --yes
supabase migration list --linkedsupabase migration list --remote
supabase gen types typescript --project-id $REFsupabase gen types typescript --remote=$REF
supabase gen types typescript --localsupabase gen types typescript
supabase functions deploysupabase functions deploy --yes
supabase secrets set --env-file .env.prodsupabase secrets set --env-file .env.prod --yes
echo y | supabase <anything>supabase <anything> --yes
supabase db remote commitsupabase db pull --remote

Two v3 changes can alter a production project without any error. config push used to apply [auth.sms] enable_confirmations inverted, and v3 applies it as written, so phone confirmation can flip on your next push. And SUPABASE_* environment variables now beat [remotes.<name>] blocks. Run supabase config diff before your first v3 config push. I didn't push config in these tests, so that part is straight from the docs.

How Do I Make CI Fail When the Target Is Wrong?

Adding --remoteeverywhere fixes today. The wrapper below keeps it fixed, because it fails the job whenever the CLI didn't report a hosted target. I ran it three ways: a remote dry run passed, a bare db push failed, and a bare migration list that exited 0 against the local database still failed the wrapper, which is the case you want caught.

#!/usr/bin/env bash
# supabase-remote.sh: run a Supabase CLI v3 command and fail the job
# unless the CLI says it targeted a hosted project.
# usage: ./supabase-remote.sh supabase db push --remote="$SUPABASE_PROJECT_REF" --yes
out=$("$@" 2>&1); status=$?
printf '%s\n' "$out"
if ! grep -q '^Target: project ' <<<"$out"; then
  echo "::error::supabase did not target a hosted project" >&2
  exit 1
fi
exit $status

And the GitHub Actions steps that use it:

- uses: supabase/setup-cli@v3
  with:
    version: 3.0.0          # or v2.stable until you've migrated

- name: Push migrations
  env:
    SUPABASE_ACCESS_TOKEN: ${{ secrets.SUPABASE_ACCESS_TOKEN }}
  run: ./scripts/supabase-remote.sh supabase db push --remote=${{ vars.SUPABASE_PROJECT_REF }} --yes

- name: Deploy functions
  env:
    SUPABASE_ACCESS_TOKEN: ${{ secrets.SUPABASE_ACCESS_TOKEN }}
  run: ./scripts/supabase-remote.sh supabase functions deploy --remote=${{ vars.SUPABASE_PROJECT_REF }} --yes

Pin the version on purpose. Without version, supabase/setup-cliinstalls npm's latest, which is v3 now, unless a lockfile at the repo root pins supabase. That's how a pipeline nobody touched breaks this week.

Should You Upgrade Now or Pin v2?

Pin CI to v2.stable today and upgrade it on your schedule.Then move your laptop to v3 first, because that's where the local-by-default change actually protects you: a bare db pushcan no longer hit production by accident. Move CI after you've added --remote=<ref>, --yesand the target check to every step. For most repos that's a 30-minute PR.

I think the new defaults are right. Supabase is designing for a world where coding agents run the CLI, and "local unless told otherwise, ask before touching production" is what you want there. The cost is a week of broken pipelines for everyone who wrote supabase db push and meant production. If you also just dealt with the legacy API keys migration or the new-table grants change, this is the third Supabase breaking change in a few weeks, so it's worth reading their breaking-changes changelog once a month.

Frequently Asked Questions

Why doesn't supabase db push update my hosted project anymore?

Supabase CLI v3 sends database commands to the local stack by default, even in a linked checkout. Add --remote (the linked project) or --remote=<project-ref>. Check the Target line the CLI prints on stderr to see where it actually went.

How do I fix HostedMutationConsentRequiredError?

Pass --yes to the command, or export SUPABASE_YES=1 in the job's shell. Piping y doesn't work, and SUPABASE_YES in supabase/.env or a .env file doesn't count. The error means nothing was changed, so re-running with --yes is safe.

How do I stay on Supabase CLI v2?

Use npm install supabase@v2.stable --save-dev, brew install supabase/tap/supabase@2, scoop install supabase-v2-stable, or version: v2.stable on supabase/setup-cli@v3. v2.stable was 2.122.1 on November 4, 2026, and v2 now gets only security and bug fixes.

What does '[inbucket] in supabase/config.toml is no longer supported' mean?

Your config.toml was created by supabase init before v2.108.0. Rename the [inbucket] section to [local_smtp] and keep the keys inside it. Until you do, v3 fails start, stop, status and every db command.

Does Supabase CLI v3 work with Claude Code or Cursor?

Yes, but it detects the agent from environment variables like CLAUDECODE and CURSOR_AGENT and never shows it a prompt. Hosted changes fail with a JSON error that tells the agent to ask you before re-running with --yes. Read-only commands work normally.

How I Tested This

November 4, 2026, on a Mac mini (Apple silicon) in Allen, Texas. Supabase CLI 3.0.0 and 2.122.1 installed from npm into separate folders, every command run with a clean environment (env -i) plus a personal access token, against one of my own hosted projects. Hosted writes were limited to a one-line throwaway Edge Function, deployed and deleted, and dry runs. The interactive and agent-mode results come from a Python pseudo-terminal. The other local stack on port 54322 belongs to a separate app of mine, and nothing was written to it. I didn't test config push, seeding, storage or the experimental supabase stack commands. If you're building on Supabase with automation around it, my Supabase + n8n lead database post shows the kind of setup these deploys feed.

Want Your Supabase Deploys to Stop Surprising You?

I build Next.js and Supabase apps with CI that checks where every migration actually lands. Send me your pipeline and I'll tell you what v3 breaks in it.

Related Posts