diff --git a/reference/cli/authentication.md b/reference/cli/authentication.md
index 50a56af1..7a66500b 100644
--- a/reference/cli/authentication.md
+++ b/reference/cli/authentication.md
@@ -42,11 +42,28 @@ For remote Operations API commands, the CLI uses the first complete authenticati
2. Credentials embedded in the `target` URL
3. `HARPER_CLI_USERNAME` and `HARPER_CLI_PASSWORD` environment variables
4. Legacy `CLI_TARGET_USERNAME` and `CLI_TARGET_PASSWORD` environment variables
-5. A token saved by `harper login`
-6. `username=` and `password=` operation parameters (legacy fallback)
+5. `HARPER_CLI_OPERATION_TOKEN` and `HARPER_CLI_REFRESH_TOKEN` environment variables, or their legacy `CLI_TARGET_` equivalents — see [Token credentials for CI/CD](#token-credentials-for-cicd)
+6. A token saved by `harper login`
+7. `username=` and `password=` operation parameters (legacy fallback)
+
+:::tip
+**Configure one credential style per context, not two.** Precedence exists to resolve a conflict, but it resolves it silently, and the ways this page describes for authentication to go wrong all need two styles live at once: a payload `username=`/`password=` pair takes over when a token stops resolving, a blank token variable hands the run to whatever saved login the machine has. Pick one and leave the others unset:
+
+| Context | Use |
+| ----------------------- | ----------------------------------- |
+| CI/CD pipeline | `HARPER_CLI_REFRESH_TOKEN` |
+| A one-off admin command | `auth_username=` / `auth_password=` |
+| Local development | `harper login` |
+
+Setting a token _and_ leaving `username=`/`password=` on the command is the combination that turns a token failure into an identity change rather than an error.
+:::
Credentials are resolved as a pair and are never combined across sources. An incomplete pair supplied with dedicated authentication parameters or in the target URL causes the command to fail. An incomplete environment-variable pair is skipped with a warning so that a saved login token can still be used.
+Entry 5 is not a two-step fallback the way 3 and 4 are: whichever token namespace is merely **set** claims the choice, so a blank `HARPER_CLI_REFRESH_TOKEN` shadows a complete `CLI_TARGET_REFRESH_TOKEN` instead of deferring to it — see below.
+
+Entries 5 and 6 authenticate with a bearer token and apply to **remote targets only**. A local operation goes over the domain socket, which the server already trusts, so token environment variables are deliberately ignored there — a token minted for one instance would otherwise be attached to every local `harper` command in that shell and rejected. Note what that trust means on a self-hosted runner: an unset or blank `target` does not fail. The CLI falls back to the target saved by a previous `harper login` on that machine, and only if there is none does it go local — where it runs as superuser on the socket's ambient trust, with no credential checked at all. So a job that loses its `HARPER_CLI_TARGET` either deploys to whatever remote that runner last logged into, or to the runner's own node. Neither is an error, and the first is the wider blast radius.
+
Before v5.2.0, `username=` and `password=` operation parameters took precedence over environment variables and saved login tokens. This could authenticate an operation as the wrong user when those fields were part of the operation payload, such as the user being created by `add_user`.
### Authentication Methods
@@ -94,9 +111,17 @@ Starting in v5.2.0, a complete environment-variable credential pair takes preced
- `HARPER_CLI_TARGET` - Sets the default `target` for CLI commands. `CLI_TARGET` is the legacy equivalent.
- `HARPER_CLI_USERNAME` and `HARPER_CLI_PASSWORD` - Preferred credential pair for the target.
- `CLI_TARGET_USERNAME` and `CLI_TARGET_PASSWORD` - Lower-priority legacy credential pair.
+- `HARPER_CLI_REFRESH_TOKEN` - Long-lived token the CLI exchanges for a fresh operation token on each run. `CLI_TARGET_REFRESH_TOKEN` is the legacy equivalent.
+- `HARPER_CLI_OPERATION_TOKEN` - A short-lived operation token supplied directly, for callers that mint their own.
Each credential namespace is independent. For example, the CLI never combines `HARPER_CLI_USERNAME` with `CLI_TARGET_PASSWORD`. If either namespace supplies only a username or only a password, that incomplete pair is skipped with a warning.
+The same rule holds for tokens: whichever namespace supplies a token owns both halves of it, so an operation token from one namespace is never paired with a refresh token from the other.
+
+The namespace is chosen by which one is **set**, not by which one has a usable value — so `HARPER_CLI_REFRESH_TOKEN=` (present but empty) claims the choice and shadows a perfectly good `CLI_TARGET_REFRESH_TOKEN`, which is never consulted. The run then falls through to the saved login token. Unset the preferred variable rather than blanking it.
+
+For a pipeline, a token is the right style: it is scoped to authentication, it can be revoked without changing the account password, and it cannot be used to log in interactively. Use it _instead of_ a password or payload credentials, not alongside them — see [Token credentials for CI/CD](#token-credentials-for-cicd).
+
**Example `.env` file**:
```bash
@@ -162,6 +187,66 @@ harper add_user \
target=https://prod-server.com:9925
```
+##### Token credentials for CI/CD
+
+
+
+Rather than storing an admin password in your CI provider, log in once locally and hand CI a **refresh token**. The CLI mints a fresh, short-lived operation token from it on every run, so the only durable secret the pipeline holds is a revocable token.
+
+`harper login --for-ci` writes the variables CI needs to **stdout** in `.env` format — and nothing else, so the output pipes cleanly. Everything a human reads (banner, prompts, status, warnings) goes to stderr:
+
+```bash
+# Set both GitHub Actions secrets in one command — the token is never displayed
+harper login --for-ci | gh secret set --env-file -
+
+# Or copy them to the clipboard to paste in by hand
+harper login --for-ci | pbcopy
+```
+
+The block it emits:
+
+```bash
+HARPER_CLI_TARGET=https://example.com:9925/
+HARPER_CLI_REFRESH_TOKEN=eyJhbGciOi...
+```
+
+Because stdout carries only these two lines, piping it keeps the token off your screen and out of your shell history — which is not true of copying it out of terminal output by hand. **Pipe it.** There is no guard on a terminal stdout, so running `harper login --for-ci` bare prints the refresh token into your scrollback, where the terminal may persist it. (Interactively the command does first ask you to confirm minting for that user, since doing so revokes any token the user already holds; that prompt is skipped when stdin is not a TTY.) If the cluster returns no refresh token, the command fails rather than emitting a half-block that would "succeed" at storing nothing.
+
+Expose the two values to the deploy step and no other credentials are needed:
+
+```yaml
+- name: Deploy
+ run: harper deploy project=my-app restart=true replicated=true
+ env:
+ HARPER_CLI_TARGET: ${{ secrets.HARPER_CLI_TARGET }}
+ HARPER_CLI_REFRESH_TOKEN: ${{ secrets.HARPER_CLI_REFRESH_TOKEN }}
+```
+
+**Refresh behavior.** The CLI mints an operation token from the refresh token when none is supplied, and again whenever the supplied one has expired. A token refreshed from an environment variable is held in memory for that invocation only — nothing is written to `~/.harperdb/credentials.json`, because there is no file entry for an environment-supplied credential. A refresh token the server rejects as **malformed or unrecognized** answers `401`, and the CLI stops with a non-zero exit and a "run harper login again" message.
+
+:::warning
+**An expired refresh token does not stop the command.** Harper answers expiry with `403`, not `401`, and the CLI's halt branch keys on `401` alone. This is a defect rather than intended behavior, tracked as [harper#2297](https://github.com/HarperFast/harper/issues/2297); this note should come out when it is fixed. Expiry is the guaranteed end state of every `--for-ci` token once `refreshTokenTimeout` elapses, so this is the failure a pipeline is most likely to meet, and it takes the continue path below rather than halting.
+
+Do not build a runbook around a non-zero exit at day 31. Watch the operation's own result instead. This is the failure the one-style rule above exists to contain: with no payload credentials on the command, an expired token fails visibly instead of silently changing identity.
+:::
+
+A `403`, and any other refresh failure — a 5xx, a timeout, a connection error — does not stop the command, and what happens next depends on which credentials you supplied:
+
+- **Refresh token only** (what `--for-ci` provisions): no bearer token is attached. The command either fails as unauthenticated or, if it also carries `username=` and `password=` operation parameters, authenticates as that pair instead — a different identity than the one you configured.
+- **An expired operation token as well**: that expired token is still attached, so the request goes out carrying it and the server rejects it. You get a 401 rather than a silent identity switch.
+
+A `200` response that contains no `operation_token` is not reported at all. A refresh failure therefore does not reliably halt a pipeline, and a zero exit is not proof that the identity you configured is the one that ran.
+
+**A blank token variable is reported, then skipped.** If a namespace is set but empty — the usual shape of a misconfigured CI secret — the CLI warns and continues down the precedence list, so the run proceeds under the saved `harper login` token if that machine has one. Treat that warning as a CI failure signal: a blank secret does not stop the run, it changes which identity performs it.
+
+**Lifetimes.** Operation tokens expire after `authentication.operationTokenTimeout` (default `1d`) and refresh tokens after `authentication.refreshTokenTimeout` (default `30d`). The pipeline needs a new refresh token when that window closes.
+
+:::warning
+**Each user holds only one valid refresh token at a time.** Harper stores a single refresh-token hash per user, so minting a new one revokes that user's previous token. A routine local `harper login` as the same account will break a pipeline holding the older token, and the failure only surfaces on the pipeline's next refresh.
+
+Create a **dedicated CI user** and run `harper login --for-ci` as that user. That scopes the pipeline's permissions to what it actually needs, and lets you revoke its access without disturbing anyone else.
+:::
+
#### Method 3: Dedicated Authentication Parameters
diff --git a/reference/cli/commands.md b/reference/cli/commands.md
index 549ef2cb..23e771c7 100644
--- a/reference/cli/commands.md
+++ b/reference/cli/commands.md
@@ -155,6 +155,7 @@ harper login
**Optional Parameters**:
- `` - The URL of the Harper instance to log in to.
+- `--for-ci` - After logging in, print CI/CD credentials to stdout. Available since v5.2.0.
**Prompts**:
@@ -164,6 +165,22 @@ You'll be asked to type in the following information:
- `` - Harper admin username.
- `` - Harper admin password.
+#### `--for-ci`
+
+
+
+Prints `HARPER_CLI_TARGET` and `HARPER_CLI_REFRESH_TOKEN` to **stdout** in `.env` format — and nothing else, so the output pipes directly into a secret store without the token being displayed. Everything else (banner, prompts, status, warnings) goes to stderr:
+
+```bash
+# Set both GitHub Actions secrets in one command
+harper login --for-ci | gh secret set --env-file -
+
+# Or copy them to paste in by hand
+harper login --for-ci | pbcopy
+```
+
+Run this as a **dedicated CI user**, not your own account: a user holds only one valid refresh token at a time, so issuing one for CI revokes any other token that user already had. See [Token credentials for CI/CD](authentication.md#token-credentials-for-cicd) for how the CLI consumes these variables and where they sit in authentication precedence.
+
### `harper logout`
Available since: v5.0.0
diff --git a/reference/configuration/options.md b/reference/configuration/options.md
index e8fdf3c7..72cd4922 100644
--- a/reference/configuration/options.md
+++ b/reference/configuration/options.md
@@ -107,7 +107,7 @@ authentication:
- `cacheTTL` — Session cache duration (ms); _Default_: `30000`
- `enableSessions` — Cookie-based sessions; _Default_: `true`
- `operationTokenTimeout` — Access token lifetime; _Default_: `1d`
-- `refreshTokenTimeout` — Refresh token lifetime; _Default_: `1d`
+- `refreshTokenTimeout` — Refresh token lifetime; _Default_: `30d`
- `logging` — Authentication event logging (Added in: v4.6.0); sub-options: `path`, `level`, `tag`, `stdStreams`. See [Logging Configuration](../logging/configuration.md)
---
diff --git a/release-notes/v5-lincoln/5.2.md b/release-notes/v5-lincoln/5.2.md
index 5e90160c..c2b5c6b9 100644
--- a/release-notes/v5-lincoln/5.2.md
+++ b/release-notes/v5-lincoln/5.2.md
@@ -58,6 +58,14 @@ New `write-transaction-queue-depth` and `read-transaction-queue-depth` metrics r
CLI Operations API commands now accept dedicated `auth_username=` and `auth_password=` parameters, allowing commands such as `add_user` and `alter_user` to authenticate as an administrator while keeping the affected user's `username=` and `password=` in the operation payload. Environment-variable credentials and saved `harper login` tokens now take precedence over the legacy `username=` and `password=` authentication fallback. Credential pairs are also resolved within one environment-variable namespace, preventing a username from `HARPER_CLI_*` from being combined with a password from legacy `CLI_TARGET_*` variables. See [CLI Authentication](/reference/v5/cli/authentication#authentication-precedence).
+### Token Credentials for CI/CD
+
+A pipeline no longer needs an admin password. `HARPER_CLI_REFRESH_TOKEN` supplies a long-lived token that the CLI trades for a fresh, short-lived operation token on every run, and `HARPER_CLI_OPERATION_TOKEN` supplies one directly for callers that mint their own. Both rank above a saved `harper login` token and above the legacy `username=`/`password=` fallback, so a configured CI identity is authoritative on a runner that also has a developer's login — as long as the token resolves. It does not always fail closed when it doesn't: see [Token credentials for CI/CD](/reference/v5/cli/authentication#token-credentials-for-cicd). A token refreshed from an environment variable is held in memory for that invocation only.
+
+`harper login --for-ci` provisions them: it prints `HARPER_CLI_TARGET` and `HARPER_CLI_REFRESH_TOKEN` to stdout in `.env` format and nothing else, so `harper login --for-ci | gh secret set --env-file -` stores both without the token being displayed.
+
+Because Harper keeps one refresh-token hash per user, issuing a token revokes that user's previous one — run `--for-ci` as a dedicated CI user rather than your own account. See [Token credentials for CI/CD](/reference/v5/cli/authentication#token-credentials-for-cicd).
+
## HTTP
### Middleware routing and ordering