Put it in a repository
The app directory holds a live Chatfuel token, so the first push is a secret question before it is a git question. What the wizard's GitHub step checks, how to do it by hand, and how to make git push the deploy command.
The wizard writes an ordinary React + Vite project and then leaves. Deploying it needs no
repository — npm run deploy works from a bare directory and always will — so the code can be
live and still sitting in an un-versioned folder on one laptop. This is the separate question.
It is a secret question before it is a git question. The app directory holds a live Chatfuel
token, and that token can read and change every bot in the account it belongs to. .gitignore
covers .env, but a .gitignore can be declined, a file of your own can appear between then and
now, and a wrong answer here is a token on the internet.
Check what would be committed
The scaffold ships with this:
node_modules/
dist/
.env
.env.*
!.env.example
.DS_Store
# Vercel CLI project link (created by `npm run deploy`).
.vercel/
# Supabase CLI working directory.
.supabase/
# `.envrc` exists for one purpose — `export CHATFUEL_TOKEN=…` — and direnv users
# never think of it as a dotenv file, so the rules above do not cover it.
.envrc
# Keys, and the server log: with AUTH_RECOVERY_LINK_LOG on, the log is the only
# place a password recovery link is ever written.
*.pem
*.key
*.log.env.example is the one .env* meant to be published — it holds the names of the variables and
none of the values. Before the first push, read the index rather than the ignore file:
git add -A
git ls-files --cached |
grep -E '(^|/)\.env|(^|/)\.vercel/|(^|/)\.aws/credentials$|(^|/)(\.npmrc|\.git-credentials|\.netrc|_netrc)$|\.(pem|key|p12|pfx|jks|keystore|ppk)$|(^|/)id_(rsa|ed25519)' |
grep -vE '\.(example|sample|template)$'Nothing back means nothing forbidden is staged. Then check by content as well as by name, because
a token can be pasted into a file that is not called .env:
TOKEN=$(grep '^CHATFUEL_TOKEN=' .env | cut -d= -f2-)
[ -n "$TOKEN" ] && git grep --cached -l -F -e "$TOKEN"That is the pair of checks the wizard runs itself. By name it refuses every file whose name starts
with .env and does not end .example, .sample or .template — .envrc included — plus
*.pem, *.key, *.p12, *.pfx, *.jks, *.keystore, *.ppk, id_rsa*, id_ed25519*,
.npmrc, .git-credentials, .netrc, _netrc, an .aws/credentials, and anything under
.vercel/.
By content it searches for the values the run itself holds, read out of the scaffold's own
variable declarations rather than a list somebody has to remember to extend: the Chatfuel token,
the Supabase secret key, the Supabase access token, and every module variable declared secret —
ADMIN_PASSWORD and PUBLISHING_SECRET among them. Anything shorter than 16 characters is
skipped. The Supabase URL, the anon key and the workspace id are declared not secret and are
never searched for: all three are shipped to the browser by design, and a gate that cries wolf
gets switched off.
The patterns go to git in a file rather than on a command line, which every process on the machine can read. And a scan that could not run stops the push instead of blessing it — a broken index and a clean tree both come back as no output, and reading the first as the second is how a live token gets published.
Push it
Say yes to Put this app on GitHub? — a question the run asks after the deploy and before the
agent handoff, because gh auth login prints a one-time code and waits for a browser, and after
the handoff an agent session may already own the terminal.
It never runs under --yes, --dry-run or --plan, and never without a TTY. A repository under
somebody's GitHub account, holding the source of an app wired to their bot, is not something a
non-interactive flag should be able to cause.
If the deploy step found that Vercel has no GitHub connection, that comes next, before the name is
asked for: the wizard says so, prints the page where you grant it, and asks Added it?. It is in
front of the name because everything past that prompt costs something — a commit, a repository
under your account, a push — while at vercel git connect, where the refusal would otherwise
arrive, all three have already happened. Answering no carries straight on: this is a convenience,
not a gate.
What happens then: it asks for a name and rewrites it the way GitHub would have silently (letters,
digits, dot, dash and underscore), asks Who can see it?, and prepares the local repository —
git init, HEAD on refs/heads/main, everything staged, the two scans above, and a first
commit. A directory that is already inside another repository is left alone; one that already has
an origin is reported rather than taken over, unless that remote is empty, which is the wizard's
own push interrupted halfway and gets offered again. If a scan finds something, the .git it
created a moment ago is removed again — in a repository that was already there, the index is reset
instead, so the staged copy of the offending file is not left one blind git commit from the
publication that was just prevented.
A directory that already had commits gets a third check, because a push publishes every commit and
not just the new one: every path the history holds on any branch, and the contents of the blobs
behind them. That content scan is bounded — 20 000 blobs, 1 MB each, 256 MB in total — and it says
how many of the repository's committed files it actually read whenever a bound was reached,
because a bounded scan that found nothing is a different promise from a clean history. A hit is reported and the push is offered anyway, defaulting to
no; under --yes the answer is no and the run says to rewrite the history or rotate those
credentials first.
For the GitHub side it wants gh. Already installed is free; otherwise it offers Homebrew on
macOS, winget or Scoop on Windows, and the official release archive everywhere else — the only
route on Linux, because every distro package manager there needs sudo and the wizard escalates
privileges on nobody's behalf. Every route ends by running the binary and seeing if it answers.
If there is no working gh at all, it asks for a GitHub token instead and uses the API. That
fallback is for the absence of a gh, not for a gh that tried and failed: a name already taken
fails identically on both paths.
Make a push deploy
Once a repository exists, connecting it means git push and npm run deploy do the same thing
and you can stop remembering which:
npm run connect-gitTwo things must already be true, and the script checks both before it calls the Vercel CLI,
because Vercel's own error for either is a menu you did not expect. The project must be linked —
.vercel/project.json, written by npm run deploy — and there must be an origin remote. An SSH
remote or one carrying a username names the same repository in a form the CLI does not accept, so
the script rewrites it to a plain https://host/owner/repo first.
Re-running is safe: vercel git connect on an already-connected project is a no-op that says so.
The commonest failure is not this app's: a Vercel account with no GitHub connection at all, which only you can grant, in a browser. The script prints Vercel's own words first and then the two places to fix it.
The wizard offers this step itself, but only when the deploy step already linked a project — without one there is nothing to connect the repository to.
And only when Vercel's GitHub connection is there. Still missing at that point, and it does not ask
whether a push should redeploy — vercel git connect would refuse on the first call — but says
what is off, where to switch it on, and the same npm run connect-git to run once you have.
Verify
npm run connect-git prints Connected — pushing to the default branch now deploys to
production. Prove it rather than believe it. Change something committed that you can see from
the outside — a heading in one of your modules, a token in src/vendor/ui/styles/tokens.css, the
logo file in public/ — then:
git add -A && git commit -m "A change I can see" && git pushWatch the deployment appear on Vercel and open the production URL. If nothing happens, the
repository is not connected and npm run connect-git says why.
Then check the repository itself for the thing that matters most:
gh api repos/{owner}/{repo}/contents/.env --jq .nameA 404 is the answer you want. Anything else means .env is in the repository and you are on the
callout below.
A token that has landed in a commit is public from that moment, and rewriting history does not
un-publish it — anything that fetched the repository in between still has it. Rotate it: generate
a new token, then run npx @chatfuel/wizard auth in the app directory, which replaces the
CHATFUEL_TOKEN line in .env and keeps every other line. Push the new value to your deployment
with npm run deploy, which overwrites the variable rather than adding a second one. Then remove
the file from the repository and fix the .gitignore that let it through.
What to read next
Rotate the token is the full procedure for the callout above.
Vercel covers what npm run deploy does and what it pushes;
scripts lists every script in the app, connect-git included.
Run it in CI, or without a terminal
--yes answers every question the wizard asks except the credentials, which have to be in the environment — and it refuses two steps outright.
Update an app to newer content
npx @chatfuel/wizard@latest update reads the lock in your app, overwrites what you never touched, and hands you the files where your edits and ours landed on the same lines.