Manual 0.8.0

Reaching a private repository

Who this is for: whoever set Revetly on a repository it cannot reach — a source that stays stale, a revetly doctor line saying a remote could not be reached, or a corpus that stopped changing. You need: a token from your forge and the ability to edit this instance's .env and revetly.toml; nothing here bills anything. Not here: what the engine may do to each repository once it can reach it — three tiers, one push target — which is engine-config-surface.md under [git], and what a stale corpus means for what you can trust (failure-and-staleness.md).

Revetly reads its sources out of git. Where those repositories are public, or sit on the same disk as the engine, nothing on this page applies — the engine fetches them, reads them, and you never declare a credential.

Where one is private, the engine needs a token. This page is what to give it, where to put it, and what revetly doctor says at each stage.

Which git hosts. Any that accepts a token over HTTPS, which is all of them: GitHub, GitLab, Bitbucket, Azure DevOps Repos, Gitea, Forgejo, a plain git server of your own. Nothing here is written against one provider's API — the engine speaks git and hands the token to git, so a host git can clone from is a host this can authenticate to.

SSH remotes are not authenticated by the engine. A git@host:org/repo.git or ssh:// remote needs a private key, an agent and a known-hosts policy, and the engine's container has none of the three. Where a credential covers an SSH remote the engine says so by name rather than trying — because presenting nothing gets Permission denied (publickey) back, which looks exactly like a token that was refused. Point the remote at its HTTPS URL, or leave that repository to whatever key is already on the machine and declare nothing.

What needs one, and what does not

The engine touches a remote in four places: a fetch before every read of a repo-backed source, a git ls-remote that watch mode uses to ask cheaply whether anything moved, a clone of the records repository where one is declared, and a push to that same records repository and to nothing else.

A repository with no remote, or one whose remote answers without asking who you are, needs no declaration and gets no warning about one. So does an instance that has declared [git] fetch = false — that is the local-only opt-out, and it means every root is read from the disk it is on, no remote contacted.

Declaring a credential

Two halves, and the split is the whole rule: revetly.toml says where the token is, the environment holds what it is. revetly.toml is committed to the repository it configures. A token in it is a token in your history.

[git.credentials.default]
match = "https://git.example.com/product/"
# in this instance's .env, which is git-ignored
REVETLY_GIT_TOKEN=...

That is the whole declaration for an instance with one credential: token_env is optional and defaults to REVETLY_GIT_TOKEN. The name is deliberately about nothing — not your forge, not your organisation, not the repository — because where your code happens to be hosted is not a property of the token. match names the host because that is what a remote is.

Declaring several means naming a variable for each, since the point of several is that one host's token is never presented to another:

[git.credentials.product]
match     = "https://gitlab.example.com/product/"
token_env = "REVETLY_GIT_TOKEN_PRODUCT"

[git.credentials.vendor]
match     = "https://github.com/vendor/"
token_env = "REVETLY_GIT_TOKEN_VENDOR"

Leaving token_env off more than one is refused at load: they would all read the same variable, and every remote would be handed the same token.

match is a URL prefix, not a path. A credential is a property of the remote: the engine clones the records repository before any local checkout of it exists, so a declaration keyed on a directory could not supply the one call that most needs it. Longest match wins, so a credential for one repository can sit beside a credential for the whole organisation.

key what it is default
match URL prefix this credential is for required
token_env the name of the environment variable holding the token REVETLY_GIT_TOKEN
username the username half of an HTTPS token x-access-token
write whether this credential may be used for a push false

A token = "..." here is refused by name, with the fix, the same way a connector's settings table refuses one.

Read and write are separate

The engine fetches many repositories and pushes to exactly one — the records clone, and [git] records_clone is the only key that makes a repository that. On an instance declaring several projects there is still one: a records_clone declared for the instance resolves against the instance, and each project writes its records into its own folder of that clone. write defaults to false, so an instance that declares tokens to read three repositories can push to none of them.

A push resolves against the credentials that declare write = true and no others. Where a read-only credential covers the push URL, the push is refused and says so, rather than quietly using the token that happened to be there.

[git.credentials.sources]                 # everything it reads
match     = "https://git.example.com/product/"
token_env = "REVETLY_GIT_TOKEN_SOURCES"

[git.credentials.records]                 # the one it writes to
match     = "https://git.example.com/product/revetly-records.git"
token_env = "REVETLY_GIT_TOKEN_RECORDS"
write     = true

Scope the tokens the same way at the forge. A read credential wants contents read access and nothing else.

A fine-grained GitHub token carries one permission set across every repository it selects, so a token that may push to the records repository may also push to everything else it can see. write = false stops this engine from trying, and it cannot un-grant what the forge granted — which is why the two credentials above name two variables and hold two differently-scoped tokens.

The username half

Most hosts ignore it entirely when the password is a token, which is why it defaults to x-access-token and most instances never set it. Two that do read it:

host username
GitHub, GitLab, Azure DevOps, Gitea anything — leave it
Bitbucket Cloud, API token x-token-auth
Bitbucket Cloud, app password your account name

A container with no shell

The engine runs as a container with no interactive shell, no keychain, no ssh-agent and no credential helper of its own. You do not need one. The credential arrives the same way the LLM key does:

  1. Put the value in /project/.env — the instance's own file, git-ignored.
  2. Declare the [git.credentials.<name>] table in revetly.toml.
  3. docker compose exec revetly revetly doctor

Nothing in docker-compose.yml changes. The engine loads this instance's .env before every check and every pass, so a token added there is live on the next command.

The token is never written into a remote URL. It is handed to git through a credential helper built for one invocation and read from the environment, which is why it does not appear in the error text git produces when a fetch fails — and that text is quoted into the pass report, the freshness row, the doctor chain and a connector's stale reason. Every failing git message is put through the redactor as well, so a URL you wrote by hand with a token in it is blanked on the way out.

What doctor says

[PASS] git credentials    default: https://git.example.com/product/ (read only), REVETLY_GIT_TOKEN is set
[PASS] git remote: product     origin/main — credential default accepted
[PASS] git push: records       origin/main — credential records may push (nothing transferred, no ref moved)

Four states for the read, and the fix differs for each:

line what it means what to do
no credential declared for it, so it is reached as a public remote nothing needed, or nothing declared yet if the fetch works, nothing
REVETLY_GIT_TOKEN is not set the declaration is there, the value is not export it — it never goes in revetly.toml
credential default was rejected (…) the remote refused the value, and the brackets carry what it said issue a new token; this one will be refused identically next pass
could not be reached (…); the credential itself was never judged weather try again

And what each of them weighs. For every credential except the one that declares write, the last three are a WARN and never a FAIL. git ls-remote is how this engine decides whether a clone is behind its remote; a credential that cannot make that call costs the freshness claim and not the read. The connector reads the revision already on disk, reports that view as incomplete, tombstones nothing, and the pass completes — which is what happened here on 2026-08-25, when a fine-grained token had not selected a newly declared repository and the pass triggered by a set_status finished normally with every artifact from that repository extracted. Doctor called it a failure and told the operator a pass would spend and fail partway; the decision that produced was to not run a pass for two hours. A diagnostic that stops correct work is worse than one that says nothing, because it is trusted.

The credential that declares write is the exception, and it is a FAIL. That is the one this engine pushes with, and a value the remote refuses there means an approved draft is committed and reaches no remote — exactly the failure this severity split exists to prevent. Nothing new declares the difference: write already did.

Both severities are the same for a missing credential as for a rejected one on the same repository. Which of the two it is changes the fix, and the fix is what the lines above differ in; it does not change the consequence, so it does not change the weight.

The first two cost nothing and need no network, so they answer under --offline. The third cannot be known without asking, so it is asked with one git ls-remote and skipped when offline.

And whether it can push, which is a different question

git ls-remote is a read. All four states above are statements about reading, and a token that fetches a remote all day may still be refused git-receive-pack — which is exactly how an approved draft gets committed locally, fails to reach the remote, and leaves a chain that says credential records accepted.

So a credential that declares write = true is asked with the operation it declares: one git push --dry-run against the records remote. It negotiates, transfers no objects and updates no ref — on a remote already up to date it says Everything up-to-date and stops there.

git push: … line what it means what to do
credential records may push the write half holds nothing
accepted for reading and refused for writing the token reads this repository and may not write it grant it write access to that repository's contents, or point token_env at one that has it — the token is not wrong, this is the one permission it lacks
the write half is undetermined the forge answered in a way that cannot be told from a refusal nothing to rotate on the strength of it; the push itself reports what happens
could not be reached weather try again
no credential declares write for the one remote it writes to a read-only credential covers the records remote declare write = true on a credential matching it

Scope, deliberately. It runs for the records remote alone and for credentials that declare write = true: a read-only credential refused a push on a doc-root remote is correct behaviour, and reporting it would put false alarms on a chain people would then learn to skip. It is a network call, so it is skipped under --offline like the rejection probe beside it. It is a WARN and never a FAIL — ingest, capture and review all still work, the commit is durable locally, and git pushes everything it is behind by as soon as the token is widened. What is lost is not the write; it is knowing.

Nothing about the check writes: no ref moves, no credential is minted or widened, and no remote configuration is changed.

Why a rejected credential is not retried

An authentication that was refused will be refused identically on the next tick and the one after. A network that was down may not be. So they are told apart and reported apart: an unreachable remote makes watch mode fall back to a full read, which is the right price for not knowing, and a rejected credential does not — it names the fix, leaves the local fingerprint to decide whether anything moved, and stops there.

What a failure does not do

An engine that cannot reach a remote reads the revision already on disk and reports that view as incomplete. That is deliberate and it is the safety property: absences in an incomplete read are not withdrawals, so nothing is tombstoned on the strength of a repository the engine could not see.

The cost is that a frozen corpus is quiet. It reports as one warning among warnings, and it stays that way until somebody reads it. If revetly status shows a source stale for days, this page is where to look first.