Diffinery

codician/diffinerypublic

Open-source, agent-first GitHub and GitHub Actions alternative that runs on your own Cloudflare account: git hosting, pull requests, code review and drop-in GitHub Actions YAML on Workers, Durable Objects, R2 and Containers.

284 lines · 22.3 KBRaw History
1---
2title: Personal access tokens
3description: Create, scope, restrict, use and revoke Diffinery personal access tokens (dfy_) for Git, the REST API, the CLI and MCP clients.
4---
5
6A personal access token lets Git, the `diffinery` CLI, scripts and MCP clients act as you. Tokens start with `dfy_`. You need one for anything outside the browser: passkeys only sign in to the web UI.
7
8## What a token is
9
10| Property | Value |
11| --- | --- |
12| Format | `dfy_` followed by 40 random characters (letters, digits, `-` and `_`) |
13| Shown | Once, when you create it. Diffinery stores only a hash. |
14| Prefix | The first 10 characters (for example `dfy_Ab12Cd`) are kept so you can recognize the token in lists and emails |
15| Scopes | `read`, `write` or `admin` (default `write`) |
16| Repository restriction | Optional list of repositories; default is every repository you can access |
17| Lifetime | 1 to 366 days (default 90) |
18| Last used | Recorded at most once an hour |
19
20A token acts as your account with your current role. If your access changes (you leave an organization, a collaborator role is removed), the token's reach changes on the next request. If your account is disabled, all your tokens are revoked.
21
22## Scopes
23
24The scope caps the role a token can use on a repository. Your effective role through a token is the lower of your own role and this cap.
25
26| Scope | Highest role | Typical use |
27| --- | --- | --- |
28| `read` | read | Clone and fetch, download Git LFS objects, read the API, CI that only downloads |
29| `write` | maintain | Push, issues, pull requests, merging, running workflows; the right choice for most Git and CLI use |
30| `admin` | admin | Everything you can do, including repository settings, collaborators, branch protection, secrets, webhooks, self-hosted runners and revoking tokens. No token can create tokens or agents |
31
32The web form describes them as: "read: clone and view · write: push, issues, pull requests, runs (up to maintain) · admin: everything you can do, including settings."
33
34### Read tokens never write
35
36A `read` token only reads. Every request that changes something in a repository (any method other than `GET` or `HEAD`) is refused with `403 this token needs the write scope`, even actions the `read` role itself allows, such as opening an issue, commenting, reacting or leaving a comment-only review. Two Git operations use `POST` but only read, and keep working with a `read` token: `git fetch` and `git clone` (`git-upload-pack`) and Git LFS downloads (an LFS batch request for `download`). Pushing and LFS uploads need `write`.
37
38The scope is checked after Diffinery has found a repository you can see, and before your role on it. So a `read` token that tries to change a private repository you can't see gets `404 repository not found`, the same answer as any other request for it, rather than a scope error that would reveal the repository exists. Use a `write` token for anything that opens, comments, pushes or merges, and an `admin` token for repository settings.
39
40### Account-level actions
41
42Actions that change your account or an organization, rather than a single repository, check the token's scope directly. Most of them also refuse tokens that are restricted to repositories, with `403 this token is limited to repositories and can't change account settings`. Browser sessions can do all of them.
43
44| Action | Token needs | Restricted tokens |
45| --- | --- | --- |
46| Create a repository (`POST /api/v3/user/repos`, `POST /api/v3/orgs/{org}/repos`, `diffinery repo create`) | `write` or `admin` | Refused |
47| Mark notifications read (`PUT /api/v3/notifications`), change notification settings, send yourself a test email | `write` or `admin` | Refused |
48| Watch or unwatch a repository (`PUT`/`DELETE /api/v3/repos/{owner}/{repo}/subscription`) | `write` or `admin` | Allowed for the listed repositories |
49| Upload or remove a profile picture (`/api/v3/user/avatar`, `/api/v3/orgs/{org}/avatar`) | `write` or `admin` | Refused |
50| Change an organization's display name or base permission (`PATCH /api/v3/orgs/{org}`, owners only) | `admin` | Refused |
51| Create a token (`POST /api/v3/diffinery/tokens`, `diffinery token create`) | No token can: a browser session only | Refused |
52| Revoke a token (`DELETE /api/v3/diffinery/tokens/{id}`, `diffinery token revoke`) | `admin` | Refused |
53| Create an organization (`POST /api/v3/user/orgs`) | `admin` | Refused |
54| Invite people (`POST /api/v3/orgs/{org}/invitations`, `POST /api/v3/invitations`, `diffinery invite`) | `admin` | Refused |
55| Accept or decline an invitation you received (`PATCH`/`DELETE /api/v3/user/repository_invitations/{id}`, `PATCH /api/v3/user/memberships/orgs/{org}`) | `write` or `admin` | Refused |
56| List, set and delete organization secrets (`/api/v3/orgs/{org}/actions/secrets`, `diffinery secret ... --org`) | `admin` | Refused |
57| Create an agent or a token for an agent (`POST /api/v3/diffinery/agents`, `POST /api/v3/diffinery/agents/{login}/tokens`, `diffinery agent create`) | No token can: a browser session only | Refused |
58| Disable an agent (`DELETE /api/v3/diffinery/agents/{login}`) | `admin` | Refused |
59| Send every email template (site administrators) | `admin` | Refused |
60| Manage passkeys and browser sessions; change your username, display name or appearance | Not possible with a token; use the browser | |
61
62A token whose scope is too low gets `403 this token needs the write scope` or `403 this token needs the admin scope`. Watching a repository with a `read` token gets `403 this needs a write or admin token`, and changing a profile picture with the wrong token gets `403 changing a profile picture needs a write or admin token that is not limited to repositories`.
63
64See [Permissions](/docs/accounts/permissions) for what each role allows.
65
66## Create a token
67
68Creating a token needs a sign-in from the last 2 hours. If yours is older, Diffinery asks you to [Confirm it's you](/docs/security/overview#confirming-its-you) with your passkey first.
69
70### In the web UI
71
721. Go to **Settings → Access tokens** (`/settings/tokens`).
732. Under **Create a token**, fill in:
74 - **Name**: what the token is for, for example `laptop`, `ci` or `claude-code` (up to 80 characters).
75 - **Expires in (days)**: 1 to 366. The default is 90.
76 - **Scope**: **Read**, **Write** (default) or **Admin**.
77 - **Restrict to repositories** (optional): pick one or more repositories. Leave it empty for every repository you can access.
783. Click **Create token**.
79
80The page shows **Your new token** with a copy button. Copy it now: it is never shown again. Diffinery also emails you "A personal access token was added to your account".
81
82Your first token has to be created in the web UI; after that you can create more from the CLI or API with an `admin` token.
83
84### With the CLI
85
86You don't need to create a token for the CLI itself: `diffinery login` signs it in through your browser and creates its own token (see [Install and sign in to the CLI](/docs/accounts/cli-sign-in)). Install it with:
87
88```sh
89curl -fsSL https://diffinery.com/install | bash
90diffinery login
91```
92
93No token can create another token, so on a deployed instance `diffinery token create` gets `403 creating tokens needs a browser session: sign in at https://diffinery.com/settings/tokens`: create tokens in the web UI. The command works against a local development server (see [Deploy](/docs/self-hosting/deploy)):
94
95```sh
96# A write token for CI, valid for 30 days, only for acme/website
97diffinery token create --name ci --scope write --expires-days 30 -R acme/website
98```
99
100| Option | Description |
101| --- | --- |
102| `-n, --name NAME` | Token name. Required. |
103| `-s, --scope SCOPE` | `read`, `write` (default) or `admin`. Repeatable, or comma-separated. |
104| `-e, --expires-days DAYS` | Days until it expires, 1 to 366 (default 90). |
105| `-R, --repo OWNER/NAME` | Restrict the token to this repository. Repeatable. |
106| `--json` | Print `token`, `id` and `expiresAt` as JSON. |
107
108The token is printed on standard output (so you can pipe it), and the ID and expiry date on standard error.
109
110### With the API
111
112`POST /api/v3/diffinery/tokens`, from a browser session (the session cookie you get by signing in with a passkey). No token can create a token, whatever its scope, so a leaked or limited token can never turn itself into a wider or longer-lived one:
113
114```sh
115curl -X POST https://diffinery.com/api/v3/diffinery/tokens \
116 -H "Cookie: __Host-dfy_session=..." \
117 -H "Content-Type: application/json" \
118 -d '{"name": "ci", "scopes": ["write"], "repositories": ["acme/website"], "expires_in_days": 30}'
119```
120
121| Field | Type | Description |
122| --- | --- | --- |
123| `name` | string | Token name |
124| `scopes` | array of strings | One or more of `read`, `write`, `admin`. Omit it (or send `null`) for the default `["write"]`. An empty list, a value that isn't a list, or an unknown name returns `422 scopes must be a non-empty list of: read, write, admin`. Repeated names are stored once. |
125| `repositories` | array of strings | `owner/name` of each repository to restrict to. Omit for every repository. You can only list repositories your account can see: a repository that doesn't exist, or a private one you have no access to, returns `422 unknown repository <name>` (the same message for both, so the answer doesn't reveal private repositories). |
126| `expires_in_days` | number | 1 to 366 (values outside are clamped). Default 90. |
127
128Response (`201 Created`):
129
130```json
131{ "token": "dfy_...", "id": "9b2f0c1d4e5a6b7c8d9e0f1a2b3c4d5e", "expiresAt": "2027-01-11T09:30:00.000Z" }
132```
133
134Any request authenticated with a token (personal, agent, job or sandbox) gets `403 creating tokens needs a browser session: sign in at https://diffinery.com/settings/tokens`. The CLI checks scope names before sending and exits with `unknown scope "<name>" (read, write or admin)`.
135
136## Restrict a token to repositories
137
138A restricted token only works on the repositories you picked. Any other repository answers `404 repository not found`, as if it didn't exist, even if you could normally read it. Inside the listed repositories the usual role and scope cap apply. Repository lists (`GET /api/v3/user/repos`, `/users/{login}/repos`, `/orgs/{org}/repos` and the MCP `list_repos` tool) show only the listed repositories, and reading notifications, which cover every repository, answers `403 this token is limited to repositories and can't read notifications`. Other account-level reads (such as `GET /api/v3/user` or listing your tokens) still work, but account-level changes don't: creating repositories or organizations, inviting people, managing agents, organization secrets, notification settings and profile pictures, and creating or revoking tokens all answer `403 this token is limited to repositories and can't change account settings` (profile pictures have their own message). See [Account-level actions](#account-level-actions).
139
140Restrict tokens you hand to CI systems, agents and third-party tools to the repositories they need.
141
142## List your tokens
143
144The **Active tokens** table on **Settings → Access tokens** shows each token's **Name** and prefix, **Scopes**, **Repositories** (or "all"), **Expires** date and **Last used** time ("never" if it hasn't been used). Revoked tokens aren't listed.
145
146```sh
147diffinery token list
148```
149
150```sh
151curl -H "Authorization: Bearer dfy_..." https://diffinery.com/api/v3/diffinery/tokens
152```
153
154The API returns an array of objects with `id`, `name`, `prefix`, `scopes`, `repoIds` (`null` when unrestricted), `expiresAt`, `lastUsedAt` and `createdAt`.
155
156## Expiry and reminder emails
157
158Every token expires; you can't create one that doesn't. After the expiry date, requests with it fail with `401 invalid, expired or revoked token`.
159
160About a week before a token expires, Diffinery emails you "Your token "<name>" expires on <date>", with the token's prefix and a **Create a new token** button. The reminder is sent once per token, and only for tokens that were created to last more than 8 days. To replace a token, create a new one, swap it in wherever the old one is used (Git credentials, CI secrets, MCP client settings), then revoke the old one.
161
162## Revoke a token
163
164- **Web UI:** click **Revoke** next to the token on **Settings → Access tokens** and confirm. You see "Token revoked."
165- **CLI:** `diffinery token revoke <id>` (the ID is in `diffinery token list`).
166- **API:** `DELETE /api/v3/diffinery/tokens/{id}`, which returns `204`. Revoking through the API or CLI needs an `admin` token that isn't restricted to repositories.
167
168A revoked token stops working immediately and can't be restored. Anything still using it gets `401 invalid, expired or revoked token`.
169
170## Use a token
171
172### Git over HTTPS
173
174Use any username and the token as the password:
175
176```sh
177git clone https://diffinery.com/acme/website.git
178# Username: diffy (anything works)
179# Password: dfy_...
180```
181
182To avoid typing it, let the CLI answer Git's credential prompts for `diffinery.com`:
183
184```sh
185diffinery login # paste the token once; saved in ~/.config/diffinery/config.json (mode 0600)
186diffinery auth setup-git # git uses the saved token for https://diffinery.com
187```
188
189`diffinery auth setup-git` configures `credential.https://diffinery.com.helper` in your global Git config. You can also use Git's own helpers, for example `git config --global credential.helper store`, or your operating system's keychain helper. See [Git](/docs/repositories/git).
190
191### curl and other HTTP clients
192
193Send the token as a bearer token:
194
195```sh
196curl -H "Authorization: Bearer dfy_..." https://diffinery.com/api/v3/user
197```
198
199Diffinery accepts these `Authorization` forms:
200
201| Form | Example |
202| --- | --- |
203| Bearer | `Authorization: Bearer dfy_...` |
204| Token (GitHub style) | `Authorization: token dfy_...` |
205| Basic | `Authorization: Basic base64(username:dfy_...)`. The password is used; the username can be anything. If the password is empty, the username is used as the token. |
206
207Git uses Basic authentication; the CLI, MCP clients and most scripts use Bearer. They are equivalent. A `Basic` value that isn't valid base64 is refused with `401 malformed basic credentials`.
208
209If a request carries the `Authorization` header twice (Git LFS does this when you set `http.extraHeader` and it also sends its own header), Diffinery uses the first one.
210
211### The CLI
212
213`diffinery login` signs the CLI in through your browser and saves the server URL and the token it gets in `~/.config/diffinery/config.json` (or under `$XDG_CONFIG_HOME`). Those tokens are listed here with a **CLI sign-in** badge and named after the computer; see [Install and sign in to the CLI](/docs/accounts/cli-sign-in). `diffinery login --token dfy_...` saves a token you created instead. The environment variables `DIFFINERY_TOKEN` and `DIFFINERY_URL` override the saved values, which is handy in CI:
214
215```sh
216export DIFFINERY_TOKEN=dfy_...
217diffinery whoami
218```
219
220`diffinery logout` removes the saved token, and revokes it when a CLI sign-in created it (a token you pasted may be in use elsewhere, so it's left alone). See [CLI reference](/docs/reference/cli).
221
222### MCP clients
223
224Point the client at `https://diffinery.com/mcp` with the token in an `Authorization` header. `diffinery mcp-config` prints a ready-made snippet:
225
226```json
227{ "mcpServers": { "diffinery": { "type": "http", "url": "https://diffinery.com/mcp", "headers": { "Authorization": "Bearer ${DIFFINERY_TOKEN}" } } } }
228```
229
230By default the snippet refers to `${DIFFINERY_TOKEN}` from the environment; `diffinery mcp-config --show-token` embeds the saved token instead. For an AI agent, create an agent account with its own token rather than giving it yours. See [MCP](/docs/agents/mcp) and [Agents](/docs/agents/overview).
231
232## Agent tokens
233
234Agents have their own `dfy_` tokens, created by their owner under **Settings → Agents** (default lifetime 30 days there), with `diffinery agent create`, or with `POST /api/v3/diffinery/agents/{login}/tokens`. Creating them through the API or CLI needs an `admin` token that isn't restricted to repositories; the `scopes` field follows the same rules as for personal tokens (default `["write"]`, `422` for an empty list or unknown name). They work like personal tokens but act as the agent, within its policy. Their creation and expiry emails go to the agent's owner (or, for an organization agent, the organization's owners and admins). See [Agents](/docs/agents/overview).
235
236## Job tokens and sandbox tokens
237
238Two other kinds of token exist. You don't create them yourself:
239
240- **Job tokens** (`djt_`) are given to each workflow job as `GITHUB_TOKEN` and `DIFFINERY_TOKEN`. They act as `diffinery-actions[bot]` on the repository the job runs in, with what the workflow's `permissions:` grant, as on GitHub: each API endpoint needs its permission (`contents`, `issues`, `pull-requests`, `statuses`, `checks` and so on), and a request without it gets `403 Resource not accessible by integration`. Without `permissions:` a job gets the repository's default, which is read-only (`contents` and `packages` read) for new repositories; an admin changes it with the repository's workflow permissions setting (`PUT /api/v3/repos/{owner}/{repo}/actions/permissions/workflow`). A job token's role never goes above write, so branch protection applies to it, and it never changes repository settings, secrets, variables, webhooks, runners or branch protection, or creates or changes workflow files. A run whose code came from an agent acts as an agent, so it can't push or merge to the default or protected branches. The token also reads other public repositories (not other private ones), uses the container registry according to `permissions: packages`, and stops working when the job finishes. See [Workflow permissions](/docs/actions/workflow-permissions) and [Secrets and variables](/docs/actions/secrets-and-variables).
241- **Sandbox tokens** (`dst_`) are given to each agent sandbox. They act as whoever started the sandbox, capped at write, on that repository, can also read other public repositories, and stop working when the sandbox closes. See [Sandboxes](/docs/agents/sandboxes).
242
243Self-hosted runners have tokens of their own too. A repository admin creates a **registration token** (`drg_`, valid for an hour), and each runner trades it for a **runner token** (`drn_`) that can claim that repository's jobs and nothing else: it can't read the repository, push or call any other API. Removing the runner ends its token. See [Manage runners](/docs/actions/runners#manage-runners).
244
245[Push protection](/docs/security/push-protection) blocks a push or commit that contains a job, sandbox or [agent task](/docs/agents/tasks) model token (`dmt_`), as it does for `dfy_` tokens, so one printed into a file or a commit message by a job or an agent isn't stored in the repository.
246
247## Security emails about tokens
248
249| Email | Sent to | When |
250| --- | --- | --- |
251| A personal access token was added to your account | You | Every time a token is created for you, from the web UI, the CLI or the API |
252| A token was created for the agent <agent> | The agent's owner, or its organization's owners and admins | A token is created for an agent |
253| Your token "<name>" expires on <date> | The same people | About 7 days before expiry (once per token, for tokens with a lifetime over 8 days) |
254
255These can't be turned off. If you get one for a token you didn't create, revoke it right away.
256
257## How tokens differ from GitHub
258
259- Diffinery has one kind of personal token with three scopes, plus an optional repository list. GitHub has classic tokens with many scopes and fine-grained tokens with per-permission settings.
260- Every token expires, after at most 366 days.
261- Tokens can only be created with the API or CLI when you already hold an `admin`-scoped token that isn't restricted to repositories; the first one comes from the web UI.
262- Git accepts a token as the password with any username, as on GitHub.
263
264## Troubleshooting
265
266| Message | Cause and fix |
267| --- | --- |
268| `401 invalid, expired or revoked token` | The token is unknown, expired or revoked, or your account is disabled or not yet active. Create a new token. |
269| `401 unrecognised token; create one under Settings → Access tokens` | The credential doesn't start with `dfy_` (or `djt_`, `dst_`, `drn_`), for example a GitHub token or a password. Use a Diffinery token from **Settings → Access tokens**. |
270| `401 authentication required` | No token was sent to a private repository. Configure Git credentials or add the `Authorization` header. |
271| `404 repository not found` | You have no access, or the token is restricted to other repositories. A `read` token that tries to change a private repository it can't see also gets this. |
272| `401 malformed basic credentials` | The `Authorization: Basic` value isn't valid base64. Encode `username:token` with base64, or send `Authorization: Bearer dfy_...` instead. |
273| `422 unknown repository <name>` when creating a token | The repository doesn't exist, or it is private and your account can't see it. Check the `owner/name` spelling and your access. |
274| `401 authentication required` on a public repository | You sent no token for a change (opening an issue, commenting, reacting, watching). Anonymous callers can only read. Send a `write` token. |
275| `403 write access required` (or another role) | Your role, or the token's scope, is too low. For `admin` actions, use an `admin` token. |
276| `403 this token needs the write scope` | A `read` token tried to change something. Use a `write` or `admin` token. |
277| `403 this token needs the admin scope` | Account-level actions such as inviting, creating organizations or agents, organization secrets, and creating or revoking tokens need an `admin` token. |
278| `403 this token is limited to repositories and can't change account settings` | The token has a repository list. Use a token without one, or the web UI. |
279| `422 scopes must be a non-empty list of: read, write, admin` | The `scopes` field was empty, not a list, or had an unknown name. |
280| `403 creating tokens needs a browser session: sign in at https://diffinery.com/settings/tokens` | Tokens can't create tokens. Create it in the web UI. |
281| `403 this needs a write or admin token` | Watching or unwatching a repository needs a `write` or `admin` token. |
282| `403 changing a profile picture needs a write or admin token that is not limited to repositories` | Use a `write` or `admin` token without a repository restriction, or the web UI. |
283| The CLI adds "token invalid or expired; run `diffinery login`" | Run `diffinery login` with a new token. If `DIFFINERY_TOKEN` is set, it overrides the saved token. |
284| Git keeps asking for a password | Your credential helper has an old token. Run `diffinery auth setup-git`, or update the stored credential. |