# 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.

Page: https://sdk.chatfuel.com/docs/concepts/content-source
Markdown: https://sdk.chatfuel.com/docs/concepts/content-source.md

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 [#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 [#where-a-run-lands]

| Outcome    | What it means                                                                                                                                     |
| ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| `resolved` | The two calls answered and the branch is ahead of the floor. The ordinary run.                                                                    |
| `cached`   | A resolution from the last ten minutes was reused, and no request was made.                                                                       |
| `pinned`   | The 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. |
| `floor`    | The 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 [#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 [#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](/docs/reference/environment).

## What the app keeps [#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`](/docs/guides/upgrade) 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 [#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](/docs/concepts/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.
