Which credential do I use?

There are three ways in. Picking the wrong one is not an error you notice on the day — you notice it weeks later, when the audit log cannot answer a question or an integration breaks because the person who created it left.

Ask one question: is there a human on whose behalf this acts?

You are Use Because
A developer wiring up Claude Code, Cursor, or any MCP client OAuth — claude mcp add, then a browser login Nothing to paste, and your groups refresh every session
A developer whose agent platform does not speak MCP, or who is scripting against the REST API A personal API key from /account Acts as you, rolls up to your seat, audited as your agent
Building a backend integration — a dashboard, a nightly job, an internal tool A service account, declared in config by an operator Its own permissions, its own seat, audited as service rather than impersonating a person

The first two answer yes, me. The third answers no, nobody — and a service account is the only correct answer to that, because everything else on this page acts as a person who will one day leave.

There is no deployment-wide key

Every agent call carries a credential that resolves to a real principal, and authorisation is evaluated per call against that principal's groups. There is no shared MCP key, no service token in a config file that everybody uses, and no way to create one.

Credentials are issued per (user, client) — not merely per user. Each is named at creation, individually revocable, and carries last-used tracking. That is what lets the audit trail say which agent acted, rather than only which person.

OAuth — the default for MCP clients

claude mcp add --transport http searchcode-private https://searchcode.example.com/v1/mcp

searchcode-private is a name local to your client and must be unique in that file: pasting over an entry that already has it replaces it silently, with no merge and no warning. It deliberately does not collide with searchcode, which is what searchcode.com publishes for the hosted product. Rename it to whatever you call this instance, or set mcp.serverName so every reader is handed your name instead.

Cursor, VS Code, Codex, OpenCode, Claude Desktop and Devin Desktop each spell this differently; Connect an agent client has every one, and /account shows them filled in for your instance.

The client registers itself, opens a browser, you approve, and it holds a token. Nothing is pasted anywhere.

mcp.auth.tokenTtl (default 8h) is the revocation latency this buys you in auth.mode: local. Behind a proxy, an identity-provider disable takes effect within seven days of the person's last browser visit; searchcode user disable takes effect at the next refresh in either mode.

A personal API key

Create it at /account in the UI. It is shown once.

curl -H "Authorization: Bearer $SEARCHCODE_TOKEN" \
  https://searchcode.example.com/api/v1/repos

It acts as you, with your groups as they were at the moment of issue, evaluated against the operator's live authorisation bindings. It rolls up to your seat — every credential a person holds counts as that one person, so there is no metering reason to share one.

The REST API is a first-class surface, not a fallback. It is the same API the UI and the MCP tools both consume; nothing reaches the index any other way. Not every agent platform speaks MCP, and reaching for curl is not a downgrade.

Two warnings, both discovered late by the people who ignore them:

Do not share a personal key between integrations

A key is issued per (user, client) precisely so that a leak in one place can be revoked without breaking the others, and so the audit trail can name which one acted. A key pasted into four places forfeits both properties: you cannot revoke it without an outage you cannot scope, and every record it produces says only "alice's key" when the question is "which of alice's four tools did that".

Create a second key. They are free, they are named, and /account lists them with last-used times so the dormant ones are visible.

Do not use a personal key for an unattended integration

When that person leaves, their key is revoked as part of offboarding and the integration breaks — at the worst possible moment, with nobody left who understands why. This is the single most common cause of a mysterious 401 six months into a deployment.

That is what service accounts are for. If no human is on whose behalf the thing acts, do not borrow one.

A service account

Declared in config by an operator, never created in the UI. What follows is the whole procedure, from nothing to a verified integration; why each step is the way it is lives in the service accounts article.

Give an application access

1. Decide the name and the repository groups. The name is what every audit record shows as the acting principal, so name the application, not the team. Access is granted per repository group — one of the repositoryGroups this instance declares — never per repository, and never by pattern.

2. Generate the token into a file, on the server. It must be random. Use this, and nothing a person chose:

sudo sh -c 'openssl rand -hex 32 > /etc/searchcode/sa-platform-dashboard-token'
sudo chown root:searchcode /etc/searchcode/sa-platform-dashboard-token
sudo chmod 0640 /etc/searchcode/sa-platform-dashboard-token

Writing it straight to a file on the server means the secret never passes through a terminal scrollback, a chat window or a ticket on the way. A trailing newline is stripped when the file is read. tokenEnv works instead of tokenFile where secrets arrive as environment variables.

3. Declare it.

serviceAccounts:
  - name: platform-dashboard
    groups: [platform]          # REPOSITORY groups, not identity groups
    role: viewer
    tokenFile: /etc/searchcode/sa-platform-dashboard-token

4. Validate, as the user the service runs as.

sudo -u searchcode searchcode --config /etc/searchcode/searchcode.yaml config validate

Running it as searchcode is what proves the token file is readable by the process that will read it. An unreadable or empty token file fails validation naming the path.

5. Let it load. Service accounts are live: the running instance picks the change up on its next configuration poll, with no restart. A configuration the instance refuses keeps the previous one serving and says so on /admin.

6. Hand the token over through your secret manager — never chat, email or a ticket — and have the application send it on every call, as either header:

curl -H "Authorization: Bearer $TOKEN" https://searchcode.example.internal/api/v1/...
curl -H "X-Api-Key: $TOKEN"           https://searchcode.example.internal/api/v1/...

The same token works on the REST API and on MCP (/v1/mcp).

7. Verify from the application's side.

curl -H "Authorization: Bearer $TOKEN" https://searchcode.example.internal/api/credentials/self

It should answer "type": "service", the account's name, and exactly the repositories you meant. ?repo=<name> explains why one repository is or is not reachable.

Change, rotate and revoke

To Do Takes effect
Change what it can reach Edit its groups or role Next request after the poll
Revoke it Delete its entry Next request after the poll
Rotate its token Write the new token to a new file and point tokenFile at it Next request after the poll

Rotating by overwriting the same file does nothing until a restart. The instance watches the configuration files, not the secret files they point at, so a new value behind an unchanged path is never noticed. Point at a new file, or restart.

Three things to know before you rely on one

Which credential is actually in play?

GET /api/credentials/self on the API and MCP listener answers it, for whatever credential the request presented:

{
  "subject": "jbloggs",
  "type": "agent",
  "source": "apikey",
  "client": "Claude Code - work laptop",
  "groups": ["engineering"],
  "repositories": ["acme/payments", "acme/web"]
}

type and source together name the mechanism, and the three do not overlap:

What is authenticating type source
A person's API key agent apikey
An OAuth access token agent oauth
A service account service apikey

client is the name somebody typed at issue for a key, and the OAuth client id for a token. repositories is what this credential reaches, from the same AllowedRepos every other surface uses. Add ?repo=<name> to ask about one: a repository this credential cannot reach answers 403 with the reason, rather than simply being absent from the list, because an absence is not a diagnosis.

It is deliberately not under /api/v1/. Credential introspection is plan 12's, not the query API's, so it does not appear in openapi.json or at /api/v1/docs, both of which are generated from the query API's definition table. /api/v1/me answers the same question about the PERSON and carries the same source field.

Before this response field existed, this was the only surface that knew, and it was documented nowhere: a trial answered "which credential is in force" by listing its MCP client's private credential store and noticing that searchcode had logged no OAuth traffic.

Lifetimes

api.apiKeys.ttl.max bounds how long a static key may live, and it defaults to 90 days deliberately: a static key survives an identity provider disable until it is revoked or expires, so a high ceiling makes the manual offboarding step load-bearing. Raise it only with that trade in mind.

api.apiKeys.requireName defaults to true — an unnamed key is unauditable. api.apiKeys.allowNoExpiry defaults to false, because a non-expiring credential is a finding in any audit.

Offboarding

Read security/questionnaire.md#offboarding before you write your offboarding runbook. The short version is that it is a two-step, and the second step is not optional.

What bounds a credential

Its issuer's groups, and its lifetime. There is no scope field, and that is deliberate rather than missing: the HTTP API is the single surface and the MCP tools are thin wrappers over it, so a surface scope would be trivially bypassable. A credential cannot grant more than the person or account it was issued for has, and it never grants a permission that person does not.

Generated from the documentation that ships with searchcode enterprise. Questions: ben@boyter.org