| 1 | --- |
| 2 | title: Personal access tokens |
| 3 | description: Create, scope, restrict, use and revoke Diffinery personal access tokens (dfy_) for Git, the REST API, the CLI and MCP clients. |
| 4 | --- |
| 5 | |
| 6 | A 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 | |
| 20 | A 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 | |
| 24 | The 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 | |
| 32 | The 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 | |
| 36 | A `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 | |
| 38 | The 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 | |
| 42 | Actions 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 | |
| 62 | A 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 | |
| 64 | See [Permissions](/docs/accounts/permissions) for what each role allows. |
| 65 | |
| 66 | ## Create a token |
| 67 | |
| 68 | Creating 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 | |
| 72 | 1. Go to **Settings → Access tokens** (`/settings/tokens`). |
| 73 | 2. 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. |
| 78 | 3. Click **Create token**. |
| 79 | |
| 80 | The 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 | |
| 82 | Your 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 | |
| 86 | You 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 |
| 89 | curl -fsSL https://diffinery.com/install | bash |
| 90 | diffinery login |
| 91 | ``` |
| 92 | |
| 93 | No 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 |
| 97 | diffinery 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 | |
| 108 | The 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 |
| 115 | curl -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 | |
| 128 | Response (`201 Created`): |
| 129 | |
| 130 | ```json |
| 131 | { "token": "dfy_...", "id": "9b2f0c1d4e5a6b7c8d9e0f1a2b3c4d5e", "expiresAt": "2027-01-11T09:30:00.000Z" } |
| 132 | ``` |
| 133 | |
| 134 | Any 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 | |
| 138 | A 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 | |
| 140 | Restrict tokens you hand to CI systems, agents and third-party tools to the repositories they need. |
| 141 | |
| 142 | ## List your tokens |
| 143 | |
| 144 | The **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 |
| 147 | diffinery token list |
| 148 | ``` |
| 149 | |
| 150 | ```sh |
| 151 | curl -H "Authorization: Bearer dfy_..." https://diffinery.com/api/v3/diffinery/tokens |
| 152 | ``` |
| 153 | |
| 154 | The 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 | |
| 158 | Every 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 | |
| 160 | About 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 | |
| 168 | A 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 | |
| 174 | Use any username and the token as the password: |
| 175 | |
| 176 | ```sh |
| 177 | git clone https://diffinery.com/acme/website.git |
| 178 | # Username: diffy (anything works) |
| 179 | # Password: dfy_... |
| 180 | ``` |
| 181 | |
| 182 | To avoid typing it, let the CLI answer Git's credential prompts for `diffinery.com`: |
| 183 | |
| 184 | ```sh |
| 185 | diffinery login # paste the token once; saved in ~/.config/diffinery/config.json (mode 0600) |
| 186 | diffinery 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 | |
| 193 | Send the token as a bearer token: |
| 194 | |
| 195 | ```sh |
| 196 | curl -H "Authorization: Bearer dfy_..." https://diffinery.com/api/v3/user |
| 197 | ``` |
| 198 | |
| 199 | Diffinery 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 | |
| 207 | Git 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 | |
| 209 | If 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 |
| 216 | export DIFFINERY_TOKEN=dfy_... |
| 217 | diffinery 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 | |
| 224 | Point 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 | |
| 230 | By 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 | |
| 234 | Agents 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 | |
| 238 | Two 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 | |
| 243 | Self-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 | |
| 255 | These 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. | |