CoreMesh

Using claude setup-token on a server without leaking it

How to run claude setup-token, store the long-lived token in a systemd EnvironmentFile, keep it out of argv and logs, and stop API key billing.

To use claude setup-token, run it in a terminal where Claude Code is installed and you can sign in to your Claude subscription. It prints a long-lived OAuth token. You give that token to Claude Code on the server as the CLAUDE_CODE_OAUTH_TOKEN environment variable. The part that matters is where you put it: a root-owned file loaded by systemd, not a command-line argument, a log line or your shell history.

I run Claude Code headless on my own server this way. The token lives in an EnvironmentFile with mode 0440. I also strip ANTHROPIC_API_KEY from the worker environment, so claude -p uses the subscription login and doesn't silently bill an API key. The rest of this post covers the command, the file layout, the unit, and what to check when it doesn't work.

What claude setup-token does#

claude setup-token is a subcommand of the Claude Code CLI. It creates a long-lived OAuth token tied to your Claude subscription. It exists for cases where an interactive login doesn't fit, such as a server, a scheduled job or a CI runner. The normal claude login stores credentials for an interactive user. A headless service has no browser to refresh or confirm anything, so a token you hand it through the environment is simpler.

Two consequences follow:

  • The token is a credential for your account's subscription access. Treat it like a password, not like a config value.
  • Anything that can read the token can use your subscription. The goal is to make the set of readers as small as possible.

The official Claude Code documentation covers installation and authentication options. Check it for the current behavior of the command, since the CLI changes between versions.

How to run claude setup-token and get the token#

The steps are short.

  1. Install Claude Code and confirm the binary is on your PATH:
bash
claude --version
  1. Run the command:
bash
claude setup-token
  1. Complete the sign-in it asks for. When it finishes, it prints the token in the terminal.
  2. Copy the token directly into the secrets file on the server (next section). Don't paste it into a chat, a ticket, a README or a shell command.

If you are working over SSH on a machine with no browser, the sign-in step is the awkward part. Whatever route you use to complete it, the rule for the resulting token is the same: it should only ever be written to the one file that needs it.

On Windows the command is the same once Claude Code is installed and the terminal can find claude. The differences are in how you store the variable. The systemd approach below is Linux-specific, so on Windows use whatever your service manager provides for protected environment values.

Where to store the token on a server#

The options differ in how easily the token leaks:

Method Problem
claude -p ... --some-flag $TOKEN style arguments Command-line arguments are visible to other users in the process list
export CLAUDE_CODE_OAUTH_TOKEN=... typed in a shell Ends up in shell history
Environment= line in a unit file Unit files are often world-readable and show up in systemctl show output
.env file inside a project directory Easy to commit, back up or serve by accident
EnvironmentFile= pointing at a restricted file The token stays in one file with tight permissions

The last row is what I use. I load the token into systemd services through an EnvironmentFile with mode 0440. It never goes into argv or logs.

Create the file with the right permissions before it contains anything, so there's no moment where the secret sits in a looser file:

bash
sudo install -m 0440 -o root -g claude-worker /dev/null /etc/claude-worker.env
sudoedit /etc/claude-worker.env

The file contains plain KEY=value lines:

ini
CLAUDE_CODE_OAUTH_TOKEN=paste-the-token-here

Mode 0440 with root as owner and a dedicated group means root and members of that group can read it and nobody else can. Keep the group small. Adding your own login to it is a decision to make deliberately, not a convenience.

Wiring it into a systemd service#

The unit references the file and never contains the token itself:

ini
[Service]
User=claude-worker
Group=claude-worker
EnvironmentFile=/etc/claude-worker.env
UnsetEnvironment=ANTHROPIC_API_KEY
ExecStart=/usr/local/bin/run-worker.sh

EnvironmentFile= is documented in the systemd.exec manual, along with UnsetEnvironment=. The unit file can then be readable by anyone without exposing the secret.

After editing, reload and restart:

bash
sudo systemctl daemon-reload
sudo systemctl restart claude-worker.service

Keep the token out of logs#

The unit is only half of it. The scripts your service runs also need to behave:

  • Don't run the worker with set -x. Shell tracing prints expanded variables.
  • Don't echo the environment for debugging. A stray env or printenv in a script writes the token to the journal.
  • Don't pass the token as an argument to any wrapper. Let the claude process inherit it from the environment.
  • If you log commands before running them, log the command name and your own arguments, not the environment.

To check that a running service has the variable without printing it, test for presence:

bash
[ -n "$CLAUDE_CODE_OAUTH_TOKEN" ] && echo "token set" || echo "token missing"

claude setup-token vs API key#

These are two different ways of paying for and authenticating Claude Code. The OAuth token from claude setup-token uses your subscription. An ANTHROPIC_API_KEY uses API billing.

The trap is that both can exist in the same environment. If ANTHROPIC_API_KEY is set, Claude Code may use it instead of your subscription login, and the first sign is a charge on the API side rather than an error. That is why I strip ANTHROPIC_API_KEY from the worker environment. With the key removed, claude -p has only the subscription token to work with, so it can't bill an API key by accident.

There are two places to do the stripping:

  • In the unit, with UnsetEnvironment=ANTHROPIC_API_KEY as shown above.
  • In a wrapper script, by launching the process through env -u:
bash
#!/usr/bin/env bash
exec env -u ANTHROPIC_API_KEY claude -p "$1"

The key may come from somewhere you didn't expect, such as a profile script, another service's environment file or a container default. If you want the subscription path only, make the removal explicit rather than assuming the variable isn't there.

If you do want API billing for a given job, that's a legitimate choice. Just make it a decision per service, with the key in that service's own environment file.

claude setup-token not working or command not found#

Most failures fall into a few buckets.

Command not found#

claude: command not found means the shell can't find the Claude Code binary. Check that Claude Code is installed for the user you're running as, and that its install directory is on that user's PATH. A service user often has a much shorter PATH than your login shell, so a command that works when you test it by hand can fail under systemd. Use the full path to claude in your ExecStart or wrapper script.

The token is set but authentication fails#

  • Confirm the variable name is exactly CLAUDE_CODE_OAUTH_TOKEN.
  • Look for stray quotes, spaces or a trailing line break pasted into the file.
  • Confirm the service user can read the file. Mode 0440 with the wrong group gives a service that starts with no token at all.
  • Confirm you reloaded and restarted after editing. systemd reads the environment file when the service starts.
  • Check for a leftover ANTHROPIC_API_KEY that overrides what you expect.

You can see which environment variable names a service received without showing values:

bash
sudo systemctl show claude-worker.service -p EnvironmentFiles

Tools built on top of Claude Code#

If you're using a wrapper or agent framework that expects a Claude token, the same principle applies: find out which environment variable or credential file it reads, and supply the token through a protected file rather than an argument. I can't speak for every wrapper's settings, so check its own documentation.

Revoking and rotating the token#

I don't have a verified one-line revoke command to give you, so I won't invent one. What I can say is how to handle it in practice:

  • Check your account's settings and the current Claude Code documentation for how to invalidate access for a token you no longer trust.
  • If the token may have leaked, treat it as compromised: replace the file contents with a freshly generated token, restart the services, and invalidate the old one.
  • After rotation, confirm every service still starts and authenticates, because they all read the same file.

Rotation is easier when the token lives in exactly one file. That's another reason not to copy it into unit files, scripts or notes.

What I'd do differently#

The thing I'd emphasize is the ANTHROPIC_API_KEY problem, because it fails silently. A setup that works can still be billing the wrong thing, and nothing will tell you. Strip the key explicitly, in the unit or the wrapper, from the start, instead of discovering later that it was inherited.

The other change I'd make is to create the environment file with its final permissions before writing the secret into it. Creating it with default permissions and tightening them afterward leaves a gap, even a short one.

Checklist#

  • Run claude setup-token once and copy the token only into the secrets file.
  • Create /etc/claude-worker.env (or similar) with mode 0440 and a small dedicated group before adding the token.
  • Reference it with EnvironmentFile= and never put the token in the unit file.
  • Remove ANTHROPIC_API_KEY with UnsetEnvironment= or env -u.
  • Keep the token out of argv, shell history and logs: no set -x, no env dumps.
  • Use full paths to claude in service scripts.
  • Reload and restart the service after any change.
  • Know how you'd rotate the token before you need to.

Quick answers

How do I run claude setup-token?

Install Claude Code, make sure claude is on your PATH, and run claude setup-token in a terminal. Complete the sign-in it asks for and it prints a long-lived token. Put that token straight into a protected environment file.

What is the difference between claude setup-token and an API key?

The token from claude setup-token uses your Claude subscription, while ANTHROPIC_API_KEY uses API billing. If both are set, you can end up billed through the key, so I remove the API key from the worker environment.

Why does claude setup-token say command not found?

The shell can't find the claude binary, usually because Claude Code isn't installed for that user or its directory isn't on PATH. Service users often have a shorter PATH, so use the full path to claude in your unit or script.

How do I keep the setup token out of logs?

Load it through a systemd EnvironmentFile with restricted permissions, not an argument or an Environment= line. Avoid set -x and any script that prints the environment.

Indra Wijaya

Builds and runs AI agents, bots and content pipelines, and writes down what works and what broke.

About · GitHub · RSS

Keep reading

New notes, no spam.

One post at a time, only when there's something worth reading. Follow in any feed reader.

Subscribe via RSS