Give an agent or CI job git access

You need: artifacts and auth composed, on Workers Paid. To see and revoke tokens from outside the app, controlplane too, and a management client connected to the environment the repository lives in.

The agent is an account

An agent that edits a repository and pushes its work. A CI job that clones on every build and pushes a tag at the end. Each needs git, and neither should carry a credential that outlives the job. Artifacts gives each one a grant, and git a token that lives fifteen minutes.

The kit has no machine identity of its own. The agent or the job is an account in your app, like any person, with its own user id. It holds your app’s credential for that account, one that requireAuth() accepts, and you keep it in your CI’s secret store. That is the only long-lived credential in this guide, and you revoke it in auth.

Give each agent and each pipeline an account of its own. Then a token’s holder names one of them, and taking one back leaves the others alone.

Create or pick the repository

The owner creates it, signed in as themselves:

POST /artifacts/repos
{ "displayName": "site-content", "remoteAccess": true }

The answer carries the physical name, a random UUID. It is the one in the clone URL, and the :name in every route below. An organization’s repository is POST /artifacts/organization/repos, made by a member whose role holds your managePower. Using Artifacts has both.

remoteAccess must be on. With it off, the mint route answers 404 and no git client reaches the repository.

Grant the agent read or write

As the owner:

POST /artifacts/repos/:name/grants
{ "userId": "<the agent's user id>", "grant": "write" }

Grant the least the job needs. A build that only clones takes read, and a read grant can never mint a write token. A job that pushes takes write. Posting again changes the grant, and DELETE /artifacts/repos/:name/grants/:userId takes it away.

Point git at the credential helper

Install the credential helper where the agent runs, or in the CI image, as git-credential-pithy on the PATH. Then, in the job:

export PITHY_APP_URL=https://app.example.com
export PITHY_APP_TOKEN=...   # the agent's credential, from your CI's secret store
git config --global credential.https://<ACCOUNT_ID>.artifacts.cloudflare.net.helper pithy
git config --global credential.https://<ACCOUNT_ID>.artifacts.cloudflare.net.useHttpPath true

Each clone, fetch and push now mints its own token. git asks the helper, the helper calls POST /artifacts/repos/:name/credentials as the agent, and the token comes back at the agent’s scope. It lives tokenTtlSeconds: 900 seconds unless you set it, 60 at the least, and 3600 at the most. A config above the hour is refused at boot. A job that outlasts one token never notices, because its next operation mints another.

useHttpPath is not optional. Without it git sends the host alone, and the helper cannot tell one repository from another.

Clone and push

Read the clone URL once, then let git do the rest:

remote=$(printf 'Authorization: Bearer %s\n' "$PITHY_APP_TOKEN" | curl -fsS -H @- \
  "$PITHY_APP_URL/artifacts/repos/<name>" | sed -n 's/.*"remote":"\([^"]*\)".*/\1/p')
git clone "$remote" work
cd work
git config user.name "Build bot"
git config user.email "build-bot@example.com"
date -u +%FT%TZ > LAST_BUILD
git add LAST_BUILD
git commit -m "Record the build."
git push

The agent’s credential reaches curl on stdin, never on its command line, where any user on the machine can read it. The clone is one mint and the push is another. With a read grant the clone lands and Cloudflare refuses the push.

See who holds a token

From a management client, behind artifacts:repos:read:

GET /artifacts/admin/repos/:name/tokens

Every token Cloudflare lists on the repository, with its scope, its state and its expiry. A token the kit minted carries minted: "pithy" and a holderId, the user id it was minted for, so the agent’s tokens name the agent. outside is a token the kit has no record of, made from Cloudflare’s dashboard or a script, and it is never given a holder. No route returns a token’s value.

artifacts:repos:read is a read, so a connection made with the default grant holds it, unless it was made before the Worker served the route. artifacts:tokens:revoke is a write and never is. --scope names the whole grant, so pass every scope the connection already holds beside the new one, or the update narrows it:

pithy dashboard connect --env prod --update --scope manifest:read --scope keys:rotate --scope artifacts:repos:read --scope artifacts:tokens:revoke

pithy dashboard status prints what a connection holds now.

Revoke one token, or every live one

Behind artifacts:tokens:revoke:

POST /artifacts/admin/repos/:name/tokens/:tokenId/revoke
POST /artifacts/admin/repos/:name/tokens/revoke
{ "idempotencyKey": "ci-runner-leak-2026-10-27", "reason": "The runner image shipped with its environment." }

Both fields are required. The key is yours, one per revocation. A retry under it answers what the first request did, with replayed: true, and revokes nothing, not even a token minted since. The same key sent for a different revocation is a 409. A token the repository does not hold is a 404, and spends no key.

The answer names the repository, the token or null for every live one, how many were revoked, the reason, and when. The first request writes one audit event: artifacts/token_revoked for one token, artifacts/tokens_revoked for every live one, at severity warning, with the management client as the actor and the reason, the key and the count as facts. A revoke of one token also records its holder. A replay writes nothing.

A revoked token is not a revoked agent. The agent still holds its grant, so its helper mints again on its next operation. To cut it off, the owner removes the grant first, then you revoke. A management client cannot remove a grant.

The owner’s break-glass

The owner needs no management client:

DELETE /artifacts/repos/:name/credentials

It revokes every live token on the repository, answers { "revoked": 3 }, and records artifacts/tokens_revoked against the owner. Every helper mints again on its next operation, and an agent whose grant you removed is refused. For an agent that keeps its grant, the break-glass costs one fresh mint.

What it does not do

Removing a grant does not revoke a token already minted. It keeps working for the rest of its life, up to tokenTtlSeconds. Remove the grant, then revoke.

It does not issue the agent’s credential. That is your app’s, and so is rotating it. If it leaks, revoke it in auth: whoever holds it can mint.

A management client cannot grant access, remove a grant, or create, fork or delete a repository. Those stay the owner’s.

It knows two scopes and nothing finer. A grant is read or write, because that is Cloudflare’s token scope. Nothing in the kit protects a branch.

It runs on Workers Paid only. On a Free plan the first create is refused at runtime. Cloudflare bills Artifacts from 14 October 2026, and every clone, fetch and push is an operation of its own, with a mint before it. Plan, prices and limits says what each route costs.

Check it worked

  • The agent’s clone lands, and its push lands with a write grant and is refused with a read one
  • GET /artifacts/admin/repos/:name/tokens lists the agent’s tokens with minted: "pithy" and its user id as holderId
  • A revoke answers revoked: 1, and the same request again answers replayed: true
  • With the grant removed and its tokens revoked, the agent’s next push fails at the helper with artifacts/not_found
  • Your audit trail holds one artifacts/token_revoked or artifacts/tokens_revoked event per revocation, with its reason
ESC