Skip to content

Latest commit

 

History

History
124 lines (88 loc) · 23.4 KB

auth.md

File metadata and controls

124 lines (88 loc) · 23.4 KB

Auth and agent runtimes

Reviewed 2026-10-18 against Anthropic's published docs and terms, each page fetched that day. Quotes are verbatim, apart from the links and markup noted; re-check the linked pages before relying on them, since policies change.

The rule this follows

Anthropic's Legal and compliance page, "Authentication and credential use" (fetched 2026-10-18):

"Anthropic does not permit third-party developers to offer Claude.ai login into their own applications, or to route requests through Free, Pro, or Max plan credentials on behalf of their users. Moreover, developers may not collect, store, or intermediate Claude.ai credentials or session tokens — sign-in to a Claude account must complete through Anthropic's own flow."

The same section, the next paragraph (right after its list):

"Nor does it prevent an end user from signing in to the unmodified Claude Code binary with their own Claude subscription, including where a platform hosts Claude Code as described under Can customers offer Claude Code in their products? above."

So, since issue #711:

  • The engine never touches a Claude credential. No dashboard page, route or paste box takes a setup token or a sign-in code, and the coordinator never stores or relays one. The four ways earlier versions did are gone: the account switch's paste box, adding a profile from a pasted token, the sign-in relayed through the dashboard (its one-time code), and Add project's token box. Their routes answer 410 Gone, and every request body the dashboard reads is refused if it holds a setup token or a sign-in code, whatever the route (src/claude-credential-guard.ts, test/claude-credential-guard.test.ts).
  • The person signs in to the CLI directly, on the machine. Each way to do that is below. Nothing in the engine blocks or disables any of them.
  • The dashboard shows status only: whether an account is set, and to a machine owner which one, from what root's claude-account.sh status last found. Never the token.

Signing the agents in, on the machine

Every method the unmodified claude has works for the agent users, run on the machine. In server mode each instance's agents run as wl-<name>-agent, so that is the user to sign in as; root (or an admin with sudo) does it there.

Method Server mode: who, and how Local mode
claude auth login (or /login in claude) as the agent user: sudo -u wl-<name>-agent -i claude auth login. Sign-in completes on Anthropic's page. The login lands in that user's own ~/.claude as yourself, in a terminal. The first-run page's Sign in to Claude runs the same command as this machine's user and shows Anthropic's sign-in link as a button; the link comes back to claude's own listener on localhost, and nothing is pasted into the page (src/claude-signin.ts)
claude setup-token run it anywhere signed in to the account, then as root: sudo bash scripts/setup/credentials.sh <name> claude (hidden prompt, or stdin). add-project.sh at a root shell asks the same not needed
API key as root: sudo bash scripts/setup/credentials.sh <name> api-key; claude reads it through apiKeyHelper as the agent user (below) agent_runtime.kind: api_key and ANTHROPIC_API_KEY
Bedrock, Vertex, Foundry claude supports them as the agent user (its own settings). The engine's runs refuse a run on them today: the runner's auth check accepts only claude.ai/oauth_token for cli and api_key/api_key_helper for api_key (src/runner.ts, "no silent billing switch"). That check is #700's to change, not this page's the same

The engine manages no Claude account (#736): it keeps no account profiles, fails over between none, and switches no login. One person signs each agent user in, by one of the methods above, or the instance uses an API key.

Existing installs. Nothing reopens.

  • Files. An earlier dashboard stored each of these, as the coordinator user, for a root script to read once: a switch's token, a profile's token, a sign-in code, and Add project's token. They went in a *-secrets dir in the coordinator user's own ~/.config. Add project's token went in the asking instance's coordinator user's home, under the new project's name: ~wl-<asker>/.config/<new>-secrets/claude-token. The others went in ~wl-<name>/.config/<name>-secrets/. The next run of claude-account.sh of any kind (the Machine page's Refresh is one) removes them, unread: in every ~wl-<name>/.config/*-secrets/ dir, as that coordinator user, only those four names (claude-token, claude-account-token, claude-profile-token, claude-login-code), and only regular files. It follows no link.

  • Asks. Add project's ask for the token (a paste_secret ask for claude-token), left open when the coordinator stopped mid-run, is withdrawn when the coordinator next starts. The withdrawal's event gives the reason, citing #711. Until then the ask is listed as not answerable, so no page offers its paste box, and the server refuses any answer to it.

  • Account profiles, failover and the switch (#736). What they left is never used to pick a login: each run is on its agent user's own login, and every ~/.claude-profiles stays denied to its commands. An instance whose runs were on another profile doesn't start, and says why. worklane doctor and the Machine page name what is left; remove it as root with sudo bash scripts/setup/claude-account.sh uninstall (each agent user's ~/.claude-profiles, as that user, then profiles.json, profile-ops.jsonl, switch.jsonl and paused), then sign the agent user in yourself if it isn't.

How agents are billed

There is one runtime: the locally installed, unmodified claude binary, headless (claude -p --output-format stream-json, --setting-sources project). There is no Agent SDK runtime. What differs is how that claude is billed:

Claude login (default) API key (option)
Set by root on the machine: a token from claude setup-token at scripts/setup/credentials.sh <name> claude's hidden prompt, or claude auth login as the agent user (above) root on the machine: scripts/setup/credentials.sh <name> api-key, which installs the key for the agent user and, once claude auth status as that user reports an API-key login, writes agent_api_key: <file> into the instance's credentials.yaml (as the coordinator user, through worklane instance set-agent-api-key; the instance's GitHub credential comes first) and refreshes the account record. add-project.sh's claude-token step counts it as the agents' login
agent_runtime cli api_key for an instance with agent_api_key (an instance's billing is set by its own credentials, not by the repo's config); a local run without an instance uses agent_runtime.kind: api_key (sdk is accepted as the old name) and ANTHROPIC_API_KEY
Where the credential lives the agent user's Claude config dir a file owned by the agent user, mode 0400, in a 0700 directory of its own
How claude gets it its own login apiKeyHelper (cat -- <file>), passed in each run's --settings and in the agent user's own Claude settings, so claude reads the key as the agent user when it starts
Preflight (every run) claude auth status must report claude.ai or oauth_token must report api_key or api_key_helper
Cost figures total_cost_usd is a list-price estimate, not plan usage estimate, close to the API bill
Governing terms Consumer Terms (subscriptions) Commercial Terms

Why apiKeyHelper and not an environment variable. A key in ANTHROPIC_API_KEY would be in the environment of claude and of every command an agent runs, and the coordinator would have to hold it to pass it on. With apiKeyHelper:

  • the coordinator passes only the file's path;

  • claude, running as the agent user, reads the file itself;

  • agent commands are denied the file by the sandbox's read rules while the sandbox is on (the lane's denyRead, src/instance.ts, laneRuns), as they are every profile's .credentials.json (src/runner.ts, withDeniedReads);

  • project commands (checks, worktree setup, full runs, suite tuning, a browser flow's app, auto-config, a deploy's trigger and verify), which run agent-written code as the agent user outside claude, always run in a sandbox of their own that hides the file, every ~/.claude* login, profile and settings file, the instance home and every file credentials.yaml names, whatever policy.yaml's sandbox says (issue #713, projectCommand in src/os/index.ts): bubblewrap on Linux, with its own PID namespace, so no process outside it can be read through /proc (a link in the agent's home whose way enters /proc or /sys is refused, since pinning that way would bind the host's /proc over the sandbox's own, issue #798); sandbox-exec on macOS. The agent user's home is read-only inside it (issue #798), so a command can't write config into it (~/.bashrc, ~/.profile, ~/.gitconfig, ~/.npmrc, ~/.ssh/config, ~/.local/bin, ...) for a later agent-user process outside the sandbox to read, such as a login shell or the agents' own claude, with the exceptions named below. On Linux its caches, ~/.cache and ~/.npm, are writable but start empty and are thrown away when the command ends, so every command downloads what it needs again; so are the masked ~/.claude* directories and any directory credentials.yaml hides under the home (each an empty tmpfs). On macOS the profile is meant to leave nothing in the home writable, nor any directory above it (the directory itself, not what else it holds), for a home configured at its own canonical path; it is untested on a real Mac, and mounts are unknown (see the list below); one reached through a symlink (/var/... is /private/var/... there), or one that can't be resolved, is refused, since macOS's sandbox matches canonical paths and would protect nothing there. A home that isn't a directory, or is /, is refused. On Linux, only the symlinks directly in the home and directly in ~/.config are walked, and a run is refused when one of them leaves the home through a link the agent user could re-aim, or through a path the agent user owns, or as a dangling link through a directory the agent user can write, or to a device in such a directory, or when its way enters /proc or /sys, or when it reaches under /dev anything but a device, or a device through a symlink, a .. or a writable directory there (pinning any of those would bind /dev and cost the sandbox its devices), or when ~/.cache or ~/.npm is a link. A link to / or to a directory holding the command's working directory is refused too, as is a directory holding more than 500 links, a way of more than 40 links, or a way through a directory the agent user can't search. Any other link of those out of the home is allowed, such as a skeleton's link into /usr/local. Inside this agent user's sandbox, what such a link reaches is read-only and every directory above it is pinned, as are the home and every directory above it, so none can be renamed away and recreated. A device it reaches isn't bound: writing a character device (/dev/null, a terminal) plants nothing, and a block device the agent user can write is named in the list below. A link to a device in a directory the agent user can write is refused, because a command could replace the device with a file. Otherwise the directories above the device are pinned too, except under /dev, where binding would cost the sandbox its devices; there, a device in or below a directory the agent user can write is refused instead, and nothing depends on those directories being sticky. A link to a directory above the home, such as ~/.x -> .., is bound read-only over the home's own mounts, so for that layout ~/.cache and ~/.npm are read-only too (a cost, not a route). What the read-only home does not stop. This is the one full list; the release notes, the PR and other docs point here and name only highlights:

    • a symlink deeper in the home (such as ~/.ssh/config pointing into a directory the agent user can write), or a symlink inside what an allowed link reaches, isn't walked: it is followed wherever it leads, and a command can write there for a later agent-user process to read (measured);
    • a hard link to a home file made outside the sandbox, before this release or since (pnpm's store makes them, and so does uv's cache, in ~/.cache), lets a command write that file through its other name, from wherever that name is. That includes files in the real ~/.cache and ~/.npm: the sandbox's throwaway mounts there hide them only by path. On Linux a command can't make a new one (Invalid cross-device link from the cache, Read-only file system in the home); on macOS that is untested;
    • a hard link to a file that an allowed link in the home reaches, made outside the sandbox, lets a command write that file through its other name the same way, although the link's target is bound read-only;
    • a FIFO or socket that already exists in the home stays usable through the read-only mount: what a command writes to it reaches the agent-user process at the other end, outside the sandbox, which may keep it or act on it. A command can't create a new one there (see issue #933, on agent-owned paths and agent-user services outside the sandbox);
    • a live agent-user process is reachable through any IPC channel outside the home too, since the sandbox shares the network namespace (abstract sockets, loopback ports), the IPC namespace and the filesystem with the agent user's processes outside it: a socket in $XDG_RUNTIME_DIR, the user's session bus, systemd --user, an abstract socket (measured with an abstract socket), a port on loopback, and the like. What a command sends reaches that process outside the sandbox. Unsharing the IPC namespace alone wouldn't close this (issue #933);
    • files outside the home that a later process outside the sandbox reads, and that a project command can write: the checkout's .claude/settings.json hooks, in a worktree and in the main checkout, where other runs start (issue #958); the checkout's .git/config (issue #926); and paths the agent user owns, such as $XDG_RUNTIME_DIR and /tmp/claude-<uid> (issue #933);
    • with allow_same_user set in the instance's root policy.yaml (admin-only, and off by default), there is no separate agent user, and project commands, a deploy's included, run without this sandbox at all;
    • a second mount of the home elsewhere, such as an admin's bind mount, is a writable way to it; so is a block device the agent user can write that holds the home's filesystem (a disk or partition), reached through a link or directly;
    • other users who can write a directory an allowed link reaches still can, including another instance's agent user through its own project commands: the pins and read-only binds hold only in this agent user's sandboxes;
    • the walk and its pins are worked out once, when a command starts: a link added to the home while a command runs (only a process outside the sandbox can add one) isn't covered for that command;
    • the sandbox protects the home configured as run_as.agent_home; nothing yet compares it with the agent user's home in passwd, which a login shell and cron use (issue #954);
    • the masked ~/.claude* directories and any directory credentials.yaml hides under the home are writable inside, as empty tmpfs mounts thrown away when the command ends: nothing written there persists, so this is no route, but "the home is read-only" doesn't hold there;
    • on macOS, the whole profile is untested on a real Mac (macOS isn't in CI), so every macOS claim here is inferred. seatbeltProfile walks no links: a link leading out of the home is followed, and what it reaches can be written. Its rule against renaming a directory above the home away (each such directory denied writes, the macOS form of the Linux pins) is unmeasured: until it is measured there, a home under a directory the agent user owns may still be replaceable from inside. And the profile has no rule about mounts, so whether a command can mount something over the home there is unknown.

    The release notes' After install section has the checks, as root, for the first two: links deeper in the home (namei -l on each) and hard links (find <agent home> -xdev -type f -links +1, ~/.cache and ~/.npm included). Neither finds a hard link to a file outside the home that an allowed link reaches. When bubblewrap can't start, the command doesn't run and says to run scripts/setup/machine.sh. What they print is redacted (src/events/redact.ts) before it reaches an event or a prompt;

  • instanceEnv still strips ANTHROPIC_API_KEY and ANTHROPIC_AUTH_TOKEN for every instance.

The coordinator never reads the file. It can, though: see the next section.

The helper is given only to runs as the instance's agent user. A lane that runs as another user (the eval lane) never gets it.

Spend caps are the same either way. Every run's command line carries --max-budget-usd whatever the billing: the role's budget_usd, and for most roles (workers, fix runs, the planner, chat, research, intake) no more than what's left of the day's daily_budget_usd, which the instance policy caps. Once the daily budget is spent, no new task starts.

doctor checks the key file exists, is owned by the agent user and is open to nobody else (0400). It never reads the file. As the coordinator user it usually can't see the file with its own permissions, and says so; run it as root to check the mode.

Who can reach the agents' credentials

The agents' Claude login (~/.claude/.credentials.json) and the API key file belong to the agent user, 0600 or 0400, in directories only it can open. Who can still reach them:

  • The agent user, which claude runs as, and owns the files. With the sandbox on (policy.yaml's default), its commands (Bash and the rest) are denied the files by the sandbox's read rules; claude itself reads them outside the sandbox, as it must. With the sandbox off, an agent's commands can read them. Project commands can't either way: they always run in their own sandbox (above).
  • Root, and any admin with sudo.
  • The coordinator user (wl-<name>), and the agent code it launches. Its sudo rule is wl-<name> ALL=(wl-<name>-agent) NOPASSWD: ALL, with the eval user added when there is one (scripts/setup/instance.sh:40): it may run any command as the agent user, and so read the agent's login and key file. Anything that gets code running as the coordinator user (the coordinator service, or a shell as that user) can do the same. The coordinator's own code never does; nothing enforces that, beyond the code. Narrowing the rule so the coordinator can run only what it needs is a later change (#737).
  • The instance's dashboard process, indirectly. It runs as the coordinator user with NoNewPrivileges=yes (scripts/setup/dashboards.sh), so it can't use the sudo rule itself. It can still write the coordinator user's files, policy.yaml among them (#726), so code that took it over could change what the coordinator later runs as the agent user. Count it with the coordinator user. The dashboard takes no credential in any case (above).

The root scripts handle the credential only while installing it: credentials.sh and add-project.sh take the token at a hidden prompt (or root's stdin) and pipe it into the agent user's own file, never as an argument and never to a log. claude-account.sh status reads the stored login as root to show which token it is (the first 8 hex digits of its sha256) and when it expires; it never shows or stores the token.

What the docs say

Scripted use of the CLI with a subscription is documented.

  • Authentication: "For CI pipelines, scripts, or other environments where interactive browser login isn't available, generate a one-year OAuth token with claude setup-token" (its colon left out). The same page: "This token authenticates with your Claude subscription and requires a Pro, Max, Team, or Enterprise plan."
  • Headless: "To run Claude Code in non-interactive mode, pass -p with your prompt and the CLI options you need" (its link and colon left out).

Collecting, storing or relaying a Claude credential is not allowed; signing in to the unmodified binary is. Both quotes are at the top of this page. Worklane spawns the user's own unmodified claude on the user's own machine. The user signs it in there, through Anthropic's own flow (or a token installed by root), and the engine never takes the credential.

Products should use API keys.

  • Agent SDK overview: "Unless previously approved, Anthropic does not allow third party developers to offer claude.ai login or rate limits for their products, including agents built on the Claude Agent SDK."
  • Legal and compliance: "Developers building products or services that interact with Claude's capabilities, including those using the Agent SDK, should use API key authentication through Claude Console or a supported cloud provider." (links and bold left out)
  • This is why Worklane never offers a login: the API-key option is a key you bring, for your own agents.

Subscription limits assume ordinary individual use.

  • Legal and compliance: "Advertised usage limits for Pro and Max plans assume ordinary, individual usage of Claude Code and the Agent SDK."
  • No primary doc we found permits or prohibits several parallel headless sessions, or unattended overnight runs, on a subscription. This is a grey area. Treat heavy unattended parallel use as your own risk under your plan's terms.

Risks and what Worklane does about them

Risk Mitigation
Silent API billing. In -p mode an ANTHROPIC_API_KEY in the environment takes precedence over a subscription login. Before each run the runner checks claude auth status and refuses to start if the auth method doesn't match the billing configured (login or API key). The agent's environment never carries ANTHROPIC_API_KEY, ANTHROPIC_AUTH_TOKEN or cloud-provider credentials for an instance; an API key reaches claude only through apiKeyHelper.
Heavy unattended use of a subscription Conservative default concurrency (agent_runtime.max_concurrency: 2), optional run_windows, a daily budget, and stopping on rate-limit events rather than waiting for the reset.
Credential handling The engine takes no Claude credential: no dashboard route or paste box accepts one, and none is stored or relayed by the coordinator. It passes only the key file's path and each profile's directory. The guardrails deny agents reading credential files (secret_paths), and the sandbox, while on, denies them the login and the key file. The coordinator user can still reach them through its sudo rule (above).
Untrusted repos Non-bare -p runs a project's hooks and MCP servers without a trust prompt. Only run Worklane on repositories you trust.
Product naming "Claude Code" may not be part of a product name. The product is "Worklane".

Your responsibility

Worklane is for running agents on your own machine, with your own login or key, on your own repositories. You are responsible for complying with the terms of your Anthropic plan. Worklane does not take, share, host or relay Claude credentials, and is not a way to provide agents to other people.