Skip to content
ChatfuelSDK
Concepts

Where the content comes from

The published wizard carries a commit and a digest per file rather than the files themselves, follows a branch from there, and refuses a branch that has moved backwards.

The wizard on npm carries no content. What ships beside the program is content.lock: the repository the files come from, one commit, and a sha256 for every file at that commit — plus the module manifests, which travel because the picker has to draw its list before anything has been fetched. The trees themselves arrive over the network on the run that needs them, into a cache, and every byte is checked against its digest before it is written. The list is built from what git tracks, because a file nobody committed is one no later run could fetch back.

The commit in the package is a floor

It is not where a run installs from. A published wizard follows main, so a fix to a module reaches people on their next npx rather than on the next release, and what the packaged commit means is the oldest content this wizard's own code is known to work against.

Resolving that takes two requests to GitHub, made before anything reads a manifest — the module picker draws from manifests, and a module added since the release is one nobody could otherwise choose. The first asks what the ref points at now; the second asks how that commit stands relative to the floor, and only ahead or identical is accepted. Two rather than one because the compare endpoint truncates its commit list from the far end, so reading the head out of it goes silently wrong once the branch is far enough ahead.

A branch that is behind or has diverged is refused out loud, naming the repository: it means a reset, a force-push, or an origin pointed at somebody else's fork, and serving the floor instead would install content while hiding that the branch is not what it claims to be.

Neither request carries a credential: the repository is public, and the ambient token from gh auth login or from CI would be spent on bytes anyone can read. The anonymous rate limit is enough because a resolution is reused for ten minutes, keyed by the floor as well as by the branch — a commit that is ahead of an older floor can be behind a newer one.

Where a run lands

OutcomeWhat it means
resolvedThe two calls answered and the branch is ahead of the floor. The ordinary run.
cachedA resolution from the last ten minutes was reused, and no request was made.
pinnedThe ref was a full 40-character sha, answered without a request and without the ancestor check — naming a commit is how a past run is reproduced.
floorThe commit the package shipped with, because something on the way did not answer. The reason is recorded and printed.

Everything that can go wrong ends at the floor: offline, rate-limited, a proxy that eats api.github.com, a mirror that does not speak the GitHub API, or a resolved commit whose index is missing. That commit's digests are in the package, so a run that falls back installs what the wizard was published with, which is a working install. The branch refusal is the one exception, because it is a fact about the repository rather than about this machine's network. doctor prints both halves on its content ref row and fails only on that refusal.

The digests for a resolved commit come from that commit, not from the package: content.index.json is committed for this, because a branch that has moved holds files the package's own list never had. It goes through the same checks a lock does, since this one came off a branch.

What the digests actually prove

That last sentence is the one worth being exact about, because it is easy to read as more than it is. On the ordinary path the digests travel with the bytes: content.index.json is fetched from the same commit, over the same connection, at the same moment as the files it describes. That proves the download was not corrupted or tampered with in transit. It cannot prove the commit is one you would have chosen.

So the trust root for a default run is TLS to raw.githubusercontent.com and api.github.com, plus whoever can push to main — the same shape as any dependency you install from a registry. The package's own digests are enforced end to end only on the offline and mirror paths, where nothing newer is being followed. content.lock is a floor, not a pin, and the name invites the opposite reading: it stops a downgrade, and it does not tie you to the bytes the package was published against. A full 40-character sha in CHATFUEL_CONTENT_REF is what does that — a run given one follows nothing.

And this content is code twice over. It becomes your app's source, which is the obvious half; the less obvious half is that a module's handoff.md, and a preset's playbook when you pass --app, are written verbatim into CLAUDE.md or AGENTS.md and handed to a coding agent running with shell access. Treat a content origin or an apps catalog that is not the default exactly as you would treat a dependency you are about to install. That is why the catalog asks before it fetches, and why --yes refuses one that only an environment variable named.

Where the files go

Into a cache directory named after the commit, so two wizard versions pinned to different commits never share one and nothing has to be invalidated. A file is written only after its bytes match the digest, and through a temporary name — the cache is trusted on every later run, so a partial download must never be able to look like a complete file. No single file may weigh more than sixteen megabytes: the digest is checked once the bytes are in memory, so the digest is not what bounds what an origin can make this process hold.

Only the paths the chosen modules need are fetched, and all of them before the app directory is created — a run that loses the network halfway leaves a partly filled cache, which the next attempt continues from, and no half-written app on your disk. The three variables that override any of this are on the environment page.

What the app keeps

Whichever commit the run landed on is written into the app's own .chatfuel/lock.json, next to the digest of every file the wizard wrote. That is what update reads later: it resolves the branch again, from the same floor, and moves the app to what it holds now. An app scaffolded from a repository checkout carries no commit at all, on purpose — a sha from somebody's working tree means nothing anywhere else — and update says so rather than fetching against a pin that will 404.

The apps catalog is outside all of this

--app fetches a preset from a separate catalog repository, and that is a shallow clone of a branch with no digests and nothing written down afterwards. Two people on the same wizard, on the same day, can be handed different preset code. The run prints the commit it got and the handoff records it; --apps-ref is how you pin one yourself.

Vendoring is what happens to these files once they land: they are copied into your app as source, and nothing in it points back here afterwards.

On this page