{"id":2976,"date":"2026-11-09T10:44:35","date_gmt":"2026-11-09T10:44:35","guid":{"rendered":"https:\/\/hostry.com\/blog\/?p=2976"},"modified":"2026-11-09T11:51:30","modified_gmt":"2026-11-09T11:51:30","slug":"claude-code-codex-login-headless-vps","status":"publish","type":"post","link":"https:\/\/hostry.com\/blog\/claude-code-codex-login-headless-vps\/","title":{"rendered":"Claude Code Login on a Headless VPS: Fix Redirect URI and OAuth Errors"},"content":{"rendered":"\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"1200\" height=\"675\" src=\"https:\/\/hostry.com\/blog\/wp-content\/uploads\/2026\/11\/claude-code-codex-login-headless-vps-cover.webp\" alt=\"Claude Code login on a headless VPS: a laptop browser hands a sign-in code to an SSH terminal instead of redirecting to localhost\" class=\"wp-image-2966\" srcset=\"https:\/\/hostry.com\/blog\/wp-content\/uploads\/2026\/11\/claude-code-codex-login-headless-vps-cover.webp 1200w, https:\/\/hostry.com\/blog\/wp-content\/uploads\/2026\/11\/claude-code-codex-login-headless-vps-cover-300x169.webp 300w, https:\/\/hostry.com\/blog\/wp-content\/uploads\/2026\/11\/claude-code-codex-login-headless-vps-cover-1024x576.webp 1024w, https:\/\/hostry.com\/blog\/wp-content\/uploads\/2026\/11\/claude-code-codex-login-headless-vps-cover-450x253.webp 450w, https:\/\/hostry.com\/blog\/wp-content\/uploads\/2026\/11\/claude-code-codex-login-headless-vps-cover-768x432.webp 768w\" sizes=\"auto, (max-width: 1200px) 100vw, 1200px\" \/><\/figure>\n\n\n\n<pre class=\"wp-block-preformatted\" style=\"font-size:14px\"><em><strong>Running claude over SSH and waiting for the laptop's browser to hand the login back is the usual first attempt, but the callback listens on the VPS, so paste the code instead. If the sign-in page rejects the request (\"Redirect URI \u2026 is not supported by client\"), make a setup-token on a laptop, then try an API key; \"Invalid OAuth Request\" usually means the URL was cut, so copy it with c.<\/strong><\/em><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">A <strong>Claude Code login<\/strong> on a server without a browser has three supported routes, numbered below: a pasted sign-in code, a one-year token from <span class=\"highlighted\">claude setup-token<\/span>, and a Console API key. A rejected redirect happens before you ever see a code, which is why it skips Route 1.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The flags and prompts quoted below were observed by installing Claude Code 2.1.319 and Codex CLI 0.167.0, the current npm releases, in a clean test environment on November 9, 2026. Error messages from GitHub reports are credited to their issues. The steps fit any Linux box you reach over SSH, such as a <strong><a href=\"https:\/\/hostry.com\/products\/vps\/\">KVM VPS kept on as a remote dev box<\/a><\/strong>. Examples use the user <span class=\"highlighted\">dev<\/span> and the host vps.example.com (203.0.113.10); put your own names in their place.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Claude Code login errors over SSH and what causes each one<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Match your message to a row; the Where column says whether it came from the browser or the VPS terminal.<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>What you see<\/th><th>Where<\/th><th>Likely cause<\/th><th>What to do<\/th><\/tr><\/thead><tbody><tr><td>Login page loads, then nothing returns to the terminal<\/td><td>Browser<\/td><td>Expected over SSH: the redirect cannot reach the VPS<\/td><td>Paste the code (Route 1)<\/td><\/tr><tr><td><code>Redirect URI https:\/\/platform.claude.com\/oauth\/code\/callback is not supported by client.<\/code><\/td><td>Browser, after &#8220;Authorization failed&#8221;<\/td><td>The authorization server rejects the code-paste redirect<\/td><td>Token from your laptop (Route 2) or API key (Route 3)<\/td><\/tr><tr><td><code>Invalid OAuth Request<\/code> with <code>Invalid response_type: missing. Expected: 'code'<\/code> or <code>Missing scope parameter<\/code><\/td><td>Browser<\/td><td>The URL reached the browser incomplete<\/td><td>Copy it with <code>c<\/code> or from <code>claude auth login<\/code><\/td><\/tr><tr><td><code>OAuth error: Invalid code. Please make sure the full code was copied<\/code><\/td><td>Terminal<\/td><td>Code expired or was cut while pasting<\/td><td>Start again and paste promptly<\/td><\/tr><tr><td><code>OAuth authentication is currently not supported.<\/code> (API Error 401)<\/td><td>Terminal<\/td><td>An OAuth token placed in <code>ANTHROPIC_AUTH_TOKEN<\/code><\/td><td>Use <code>CLAUDE_CODE_OAUTH_TOKEN<\/code><\/td><\/tr><tr><td><code>This organization has been disabled<\/code> (API Error 400) with an active plan<\/td><td>Terminal<\/td><td>An old <code>ANTHROPIC_API_KEY<\/code> overrides your subscription<\/td><td>Unset the key<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Row one is the ordinary SSH case. On the VPS, Claude Code opens its OAuth callback listener on 127.0.0.1 with a port the system assigns for each attempt, and that address exists only inside the server. To see it, run <span class=\"highlighted\">ss -ltnp | grep claude<\/span> in a second SSH session while the login waits; the port changes from one attempt to the next.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">For that reason, the URL Claude Code gives you to copy uses a different redirect, to platform.claude.com, which shows a code in the browser for you to paste back. Anthropic&#8217;s <strong><a href=\"https:\/\/code.claude.com\/docs\/en\/troubleshoot-install\" target=\"_blank\" rel=\"noopener\">install and login troubleshooting page<\/a><\/strong> describes exactly this for WSL2, SSH sessions and containers.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">&#8220;Redirect URI is not supported by client&#8221;<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Switch routes rather than retrying the same URL<\/strong>, because nothing on the VPS fixes a rejected redirect. Run <span class=\"highlighted\">claude setup-token<\/span> on a laptop that has a browser (Route 2). There the browser returns to Claude Code through its local listener, and the code-paste redirect is not part of the flow. If the laptop gets the same rejection, use an API key (Route 3).<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">GitHub reports of this message cover Claude Code 2.1.79 through 2.1.267, from desktops as well as from an Ubuntu VPS and an HPC cluster reached over SSH. In <strong><a href=\"https:\/\/github.com\/anthropics\/claude-code\/issues\/93216\" target=\"_blank\" rel=\"noopener\">issue #93216<\/a><\/strong> one reporter&#8217;s 2.1.251 signed in on September 9, 2026 and failed the next day with no local change, and according to the thread, downgrading did not help. Reporters read it as a server-side rejection, and the docs we checked on November 9 do not list it. The laptop route is not a guaranteed way out either: one commenter on <strong><a href=\"https:\/\/github.com\/anthropics\/claude-code\/issues\/36215\" target=\"_blank\" rel=\"noopener\">issue #36215<\/a><\/strong> saw the localhost redirect rejected the same way.<\/p>\n\n\n\n<aside class=\"hostry-author-note\" style=\"margin:26px 0;padding:16px 20px 14px;background:#fbf7ef;border-left:3px solid #eea416;border-radius:0 10px 10px 0;font-size:15px;line-height:1.6;color:#2b2f3a\">\n<div style=\"font-size:11px;font-weight:700;letter-spacing:.08em;text-transform:uppercase;color:#b07a06;margin:0 0 6px\">From the engineer<\/div>\n<p style=\"margin:0 0 12px\">If the sign-in page rejects the redirect, do not spend the evening retrying the same URL or patching the CLI. Switch to a token made on your laptop and try the interactive login again after the next release.<\/p>\n<div style=\"display:flex;align-items:center;gap:10px;font-size:13px;color:#5b6170\"><span style=\"display:inline-flex;align-items:center;justify-content:center;width:30px;height:30px;border-radius:50%;background:#11235e;color:#fff;font-size:12px;font-weight:700;flex:none\">SY<\/span><span><strong style=\"color:#11235e\">Stas Yantselovskiy<\/strong> \u00b7 System administrator &amp; DevOps engineer, Hostry<\/span><\/div>\n<\/aside>\n\n\n\n<h3 class=\"wp-block-heading\">&#8220;Invalid OAuth Request&#8221; and a URL cut by line wrapping<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Both variants name a parameter the server did not accept. <strong><a href=\"https:\/\/github.com\/anthropics\/claude-code\/issues\/29983\" target=\"_blank\" rel=\"noopener\">Issue #29983<\/a><\/strong>, filed from Docker containers on 2.1.63, tied them to the login URL&#8217;s parameters on that version and notes that terminal line wrapping can also truncate the URL. In 2.1.319, the setup-token URL runs across five lines in an 80-column window, and <span class=\"highlighted\">response_type<\/span> and <span class=\"highlighted\">scope<\/span> both sit after the first break. Select only the top line and the server sees a request without them, which produces one of these two messages.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1200\" height=\"800\" src=\"https:\/\/hostry.com\/blog\/wp-content\/uploads\/2026\/11\/claude-code-codex-login-headless-vps-routes.webp\" alt=\"Three Claude Code login routes for a headless VPS: pasted sign-in code, setup-token carried from a laptop, and a Console API key\" class=\"wp-image-2972\" srcset=\"https:\/\/hostry.com\/blog\/wp-content\/uploads\/2026\/11\/claude-code-codex-login-headless-vps-routes.webp 1200w, https:\/\/hostry.com\/blog\/wp-content\/uploads\/2026\/11\/claude-code-codex-login-headless-vps-routes-300x200.webp 300w, https:\/\/hostry.com\/blog\/wp-content\/uploads\/2026\/11\/claude-code-codex-login-headless-vps-routes-1024x683.webp 1024w, https:\/\/hostry.com\/blog\/wp-content\/uploads\/2026\/11\/claude-code-codex-login-headless-vps-routes-450x300.webp 450w, https:\/\/hostry.com\/blog\/wp-content\/uploads\/2026\/11\/claude-code-codex-login-headless-vps-routes-768x512.webp 768w\" sizes=\"auto, (max-width: 1200px) 100vw, 1200px\" \/><figcaption>Three ways in without a browser on the server. Only Route 1 needs the browser and the VPS terminal open at the same moment.<\/figcaption><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\">Route 1: copy the login URL and paste the code back<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Use this route for interactive work on a Pro, Max, Team or Enterprise plan. Install with the native installer, which puts the binary at <span class=\"highlighted\">~\/.local\/bin\/claude<\/span> and updates itself in the background:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>ssh dev@vps.example.com\ncurl -fsSL https:\/\/claude.ai\/install.sh | bash\nclaude --version\nclaude auth login<\/code><\/pre>\n\n\n\n<ol class=\"wp-block-list\"><li><span class=\"highlighted\">claude auth login<\/span> prints &#8220;Opening browser to sign in\u2026&#8221;, then &#8220;If the browser didn&#8217;t open, visit:&#8221; and the full URL, then waits at <span class=\"highlighted\">Paste code here if prompted &gt;<\/span>.<\/li><li>Open that URL in the browser on your laptop and sign in with the account that holds the plan.<\/li><li>The browser shows a code instead of redirecting. Copy all of it.<\/li><li>Paste it at the prompt in the SSH window and press Enter. <span class=\"highlighted\">claude auth login<\/span> reads the code from standard input, which helps when a terminal&#8217;s paste shortcut does not reach the interactive prompt.<\/li><\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">Inside the full-screen <span class=\"highlighted\">claude<\/span> session the same flow runs from <span class=\"highlighted\">\/login<\/span>; press <span class=\"highlighted\">c<\/span> to copy the URL. On a VPS with no clipboard tool, that copy reaches your terminal as an OSC 52 escape sequence, which some terminals block by default; iTerm2 needs clipboard access allowed in its settings.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Sign in before you start tmux: inside tmux, Claude Code 2.1.319 puts the copied URL in the tmux paste buffer instead of sending it to your laptop. For Console (API billing) accounts, run <span class=\"highlighted\">claude auth login &#8211;console<\/span>; the code step is the same.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\" style=\"font-size:14px\"><strong>Check the sign-in<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>claude auth status; echo \"exit $?\"<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">A working subscription login prints JSON with <span class=\"highlighted\">&#8220;loggedIn&#8221;: true<\/span> and <span class=\"highlighted\">&#8220;authMethod&#8221;: &#8220;claude.ai&#8221;<\/span>, and the exit code is 0; without a usable credential the command exits with 1. Inside a session, <span class=\"highlighted\">\/status<\/span> shows the same login and marks any credential that is configured but not in use. Routes 2 and 3 reuse this check.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Why an SSH tunnel to the localhost callback does not apply here<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Forwarding a port with <span class=\"highlighted\">ssh -L<\/span> needs a port number you know before you connect. Claude Code&#8217;s docs name no fixed callback port, and the 2.1.319 listener takes whatever port the kernel assigns at each attempt. The copied URL does not use localhost at all, so the pasted code covers the case a tunnel would. Codex CLI listens on localhost port 1455 by default, so a tunnel does work there.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Route 2: claude setup-token and CLAUDE_CODE_OAUTH_TOKEN for unattended runs<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">A <span class=\"highlighted\">\/login<\/span> credential that expires and cannot refresh stops an agent that runs at 3 a.m. until someone signs in again. For cron jobs and <span class=\"highlighted\">claude -p<\/span> runs, Anthropic&#8217;s <strong><a href=\"https:\/\/code.claude.com\/docs\/en\/authentication\" target=\"_blank\" rel=\"noopener\">authentication documentation<\/a><\/strong> offers <span class=\"highlighted\">claude setup-token<\/span>, which mints a one-year OAuth token tied to your subscription, so it needs the same paid plans as Route 1.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Run it on the laptop. It opens the same browser flow as \/login. The token prints once and is stored nowhere. Copy it straight into a root-owned file on the VPS:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># on the VPS\nsudo install -m 600 -o root -g root \/dev\/null \/etc\/claude-agent.env\nsudo nano \/etc\/claude-agent.env\n# one line inside:\n# CLAUDE_CODE_OAUTH_TOKEN=your-token<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">systemd reads <span class=\"highlighted\">EnvironmentFile<\/span> before it switches to the service user, so <span class=\"highlighted\">dev<\/span> never needs read access to the file:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># \/etc\/systemd\/system\/claude-todo.service\n[Unit]\nDescription=Claude Code TODO summary\n\n[Service]\nType=oneshot\nUser=dev\nWorkingDirectory=\/home\/dev\/app\nEnvironmentFile=\/etc\/claude-agent.env\nExecStart=\/home\/dev\/.local\/bin\/claude -p \"List the open items in TODO.md\"\nStandardOutput=append:\/home\/dev\/claude-todo.log<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Test it with <span class=\"highlighted\">sudo systemctl start claude-todo<\/span>, read <span class=\"highlighted\">\/home\/dev\/claude-todo.log<\/span>, then schedule it for 03:00 nightly:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># \/etc\/systemd\/system\/claude-todo.timer\n[Unit]\nDescription=Run claude-todo nightly\n\n[Timer]\nOnCalendar=*-*-* 03:00:00\nPersistent=true\n\n[Install]\nWantedBy=timers.target<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo systemctl daemon-reload\nsudo systemctl enable --now claude-todo.timer\nsystemctl list-timers claude-todo.timer<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">A systemd unit runs its command without a shell, so it never reads the exports in <span class=\"highlighted\">~\/.bashrc<\/span>. A forgotten <span class=\"highlighted\">ANTHROPIC_API_KEY<\/span> in that file therefore cannot override the token and move the job onto Console billing. In an interactive shell with the token exported, the check from the end of Route 1 should report <span class=\"highlighted\">&#8220;authMethod&#8221;: &#8220;oauth_token&#8221;<\/span>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Model requests are all this token covers.<\/strong> Remote Control and claude.ai connectors need a regular claude.ai sign-in, so a job that depends on them stays on Route 1. MCP servers, which give Claude Code extra tools and data sources, still work when defined locally on the VPS, so they do not change the choice. <span class=\"highlighted\">&#8211;bare<\/span> does: Anthropic&#8217;s minimal startup mode for scripts skips the auto-discovery of hooks, plugins, MCP servers and CLAUDE.md, and never reads <span class=\"highlighted\">CLAUDE_CODE_OAUTH_TOKEN<\/span>, so a <span class=\"highlighted\">&#8211;bare<\/span> job belongs on Route 3. Write the creation date in your calendar: at the one-year mark, generate a new token, replace the line and restart the job.<\/p>\n\n\n\n<aside class=\"hostry-author-note\" style=\"margin:26px 0;padding:16px 20px 14px;background:#fbf7ef;border-left:3px solid #eea416;border-radius:0 10px 10px 0;font-size:15px;line-height:1.6;color:#2b2f3a\">\n<div style=\"font-size:11px;font-weight:700;letter-spacing:.08em;text-transform:uppercase;color:#b07a06;margin:0 0 6px\">From the engineer<\/div>\n<p style=\"margin:0 0 12px\">For anything that runs unattended I would not rely on the interactive login. Make the setup-token once, keep it in a root-only file the service reads, and put its expiry date in the calendar the same day.<\/p>\n<div style=\"display:flex;align-items:center;gap:10px;font-size:13px;color:#5b6170\"><span style=\"display:inline-flex;align-items:center;justify-content:center;width:30px;height:30px;border-radius:50%;background:#11235e;color:#fff;font-size:12px;font-weight:700;flex:none\">SY<\/span><span><strong style=\"color:#11235e\">Stas Yantselovskiy<\/strong> \u00b7 System administrator &amp; DevOps engineer, Hostry<\/span><\/div>\n<\/aside>\n\n\n\n<h2 class=\"wp-block-heading\">Route 3: ANTHROPIC_API_KEY on the server, billed through the Console<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">A key from the Claude Console needs no OAuth flow at all, which makes it the fallback when the redirect error blocks both other routes. Usage goes on the Console organization&#8217;s bill.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The catch is that Claude Code picks a credential in a fixed order, and <span class=\"highlighted\">ANTHROPIC_API_KEY<\/span> ranks above both <span class=\"highlighted\">CLAUDE_CODE_OAUTH_TOKEN<\/span> and a <span class=\"highlighted\">\/login<\/span> subscription. With <span class=\"highlighted\">-p<\/span> it uses the key whenever one is set, and a stale key from an old project then produces the &#8220;organization has been disabled&#8221; error from the table.<\/p>\n\n\n\n<ol class=\"wp-block-list\"><li>Create a key on the <strong><a href=\"https:\/\/platform.claude.com\/settings\/keys\" target=\"_blank\" rel=\"noopener\">API keys page of the Claude Console<\/a><\/strong> and copy it once.<\/li><li>For a systemd job, put <span class=\"highlighted\">ANTHROPIC_API_KEY=your-key<\/span> in the root-only <span class=\"highlighted\">\/etc\/claude-agent.env<\/span> from Route 2, in place of the token line rather than next to it.<\/li><li>For an interactive shell, type it with <span class=\"highlighted\">read -rs ANTHROPIC_API_KEY &amp;&amp; export ANTHROPIC_API_KEY<\/span>, so the key never lands in shell history.<\/li><li>Run the check from the end of Route 1. It should report <span class=\"highlighted\">&#8220;authMethod&#8221;: &#8220;api_key&#8221;<\/span>; the first interactive session also asks you to approve the key.<\/li><\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">To find a stale key that overrides your subscription, look at what is active and where the variable comes from:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>claude auth status --text      # which credential is active\ngrep -n ANTHROPIC_API_KEY ~\/.bashrc ~\/.profile\nunset ANTHROPIC_API_KEY<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">One more trap: <span class=\"highlighted\">ANTHROPIC_AUTH_TOKEN<\/span> is meant for LLM gateways that take bearer tokens, and a subscription token pasted there fails with the 401 above.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1200\" height=\"800\" src=\"https:\/\/hostry.com\/blog\/wp-content\/uploads\/2026\/11\/claude-code-codex-login-headless-vps-precedence.webp\" alt=\"Claude Code credential precedence on a server: cloud provider credentials, ANTHROPIC_AUTH_TOKEN, ANTHROPIC_API_KEY, apiKeyHelper, CLAUDE_CODE_OAUTH_TOKEN, Anthropic profiles, then the \/login subscription\" class=\"wp-image-2973\" srcset=\"https:\/\/hostry.com\/blog\/wp-content\/uploads\/2026\/11\/claude-code-codex-login-headless-vps-precedence.webp 1200w, https:\/\/hostry.com\/blog\/wp-content\/uploads\/2026\/11\/claude-code-codex-login-headless-vps-precedence-300x200.webp 300w, https:\/\/hostry.com\/blog\/wp-content\/uploads\/2026\/11\/claude-code-codex-login-headless-vps-precedence-1024x683.webp 1024w, https:\/\/hostry.com\/blog\/wp-content\/uploads\/2026\/11\/claude-code-codex-login-headless-vps-precedence-450x300.webp 450w, https:\/\/hostry.com\/blog\/wp-content\/uploads\/2026\/11\/claude-code-codex-login-headless-vps-precedence-768x512.webp 768w\" sizes=\"auto, (max-width: 1200px) 100vw, 1200px\" \/><figcaption>The highest credential present wins, so an API key left in a shell profile outranks a setup-token or your subscription login.<\/figcaption><\/figure>\n\n\n\n<aside class=\"hostry-author-note\" style=\"margin:26px 0;padding:16px 20px 14px;background:#fbf7ef;border-left:3px solid #eea416;border-radius:0 10px 10px 0;font-size:15px;line-height:1.6;color:#2b2f3a\">\n<div style=\"font-size:11px;font-weight:700;letter-spacing:.08em;text-transform:uppercase;color:#b07a06;margin:0 0 6px\">From the engineer<\/div>\n<p style=\"margin:0 0 12px\">Keep one credential per job on the server. When an API key, a token and a \/login session all live on the same box, \/status is the first thing I would check the moment the bill looks odd.<\/p>\n<div style=\"display:flex;align-items:center;gap:10px;font-size:13px;color:#5b6170\"><span style=\"display:inline-flex;align-items:center;justify-content:center;width:30px;height:30px;border-radius:50%;background:#11235e;color:#fff;font-size:12px;font-weight:700;flex:none\">SY<\/span><span><strong style=\"color:#11235e\">Stas Yantselovskiy<\/strong> \u00b7 System administrator &amp; DevOps engineer, Hostry<\/span><\/div>\n<\/aside>\n\n\n\n<h2 class=\"wp-block-heading\">Still failing after a clean sign-in<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">When a Claude Code login keeps failing on the VPS, work through these documented steps in order and stop at the first one that changes the result:<\/p>\n\n\n\n<ol class=\"wp-block-list\"><li>Update. Native installs take <span class=\"highlighted\">claude update<\/span>; npm installs need <span class=\"highlighted\">npm install -g @anthropic-ai\/claude-code@latest<\/span>. Compare <span class=\"highlighted\">claude &#8211;version<\/span> with the version in the issue you are matching.<\/li><li>Reset the login: run <span class=\"highlighted\">\/logout<\/span>, quit, start <span class=\"highlighted\">claude<\/span> again and sign in. Logging out also clears saved MCP sign-ins.<\/li><li>Check the clock with <span class=\"highlighted\">timedatectl<\/span>. Token validation depends on correct time, so the output should show the system clock as synchronized.<\/li><li>Test with a clean directory: <span class=\"highlighted\">CLAUDE_CONFIG_DIR=~\/.claude-test claude<\/span> keeps its own credentials, which rules out a damaged <span class=\"highlighted\">~\/.claude\/.credentials.json<\/span>.<\/li><li>Run <span class=\"highlighted\">claude doctor<\/span> for read-only install and settings diagnostics.<\/li><\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">Skip the edits to the CLI&#8217;s own code that some issue threads pass around; the next update overwrites them, and nobody has reviewed the patched client. If the redirect error persists on the laptop as well, it is an account question for Anthropic support, reached through Get help in claude.ai.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Keep sessions alive and tokens out of reach<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">An SSH drop ends a foreground <span class=\"highlighted\">claude<\/span> session. tmux keeps it running on the VPS:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>tmux new -s agent        # start, then run claude inside\n# detach with Ctrl+b, then d\ntmux attach -t agent     # reattach after reconnecting<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Claude Code 2.1.319 also has <span class=\"highlighted\">claude &#8211;bg<\/span>, which starts a background session and prints an id for <span class=\"highlighted\">claude attach<\/span>. Both stop when the credential expires. Since 2.1.318, <span class=\"highlighted\">claude auth status<\/span> reports the end date of a stored claude.ai login as <span class=\"highlighted\">loginExpiresAt<\/span>. A weekly cron entry can print it:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo apt install -y jq    # if jq is not installed yet\nclaude auth status --json | jq -r '.loginExpiresAt'<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The field reads <span class=\"highlighted\">null<\/span> when the active credential is a setup-token, whose date you track yourself. Claude Code also warns three days ahead with &#8220;Your login expires in 3 days \u00b7 run \/login to renew&#8221;.<\/p>\n\n\n\n<ul class=\"wp-block-list\"><li>Check permissions with <span class=\"highlighted\">stat -c &#8216;%a %n&#8217; ~\/.claude\/.credentials.json<\/span>. It should read 600, as Claude Code writes it.<\/li><li>Keep tokens out of shell history. Type them with <span class=\"highlighted\">read -rs CLAUDE_CODE_OAUTH_TOKEN<\/span>, then <span class=\"highlighted\">export CLAUDE_CODE_OAUTH_TOKEN<\/span>, rather than writing them on the command line.<\/li><li>Never commit an env file, and never paste a token into a ticket or an issue report. If an API key leaks anyway, delete it in the Console at once.<\/li><li>None of these logins needs an inbound port beyond SSH. Restrict port 22 to your own address in the VPS control panel&#8217;s firewall, and keep the panel&#8217;s VNC console in mind if a rule locks you out.<\/li><\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Use key-only SSH and automatic security updates on the VPS. Our <strong><a href=\"https:\/\/hostry.com\/blog\/secure-new-linux-vps-first-30-minutes\/\">walkthrough for hardening Ubuntu 24.04 before any workload<\/a><\/strong> takes about half an hour.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Codex CLI login on the same headless VPS<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Codex CLI handles the server case more directly. A plain <span class=\"highlighted\">codex login<\/span> starts a local server on port 1455 and ends with the hint &#8220;On a remote or headless machine? Use `codex login &#8211;device-auth` instead.&#8221; OpenAI&#8217;s <strong><a href=\"https:\/\/learn.chatgpt.com\/docs\/auth.md\" target=\"_blank\" rel=\"noopener\">Codex authentication docs<\/a><\/strong> recommend that device code flow, still marked beta, for remote and headless machines.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>npm install -g @openai\/codex     # or: curl -fsSL https:\/\/chatgpt.com\/codex\/install.sh | sh\ncodex login --device-auth\ncodex login status<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The CLI prints a link and a one-time code that expires in 15 minutes. Open the link on the laptop and sign in, then type the code. For a personal account, device code login must first be enabled in ChatGPT&#8217;s security settings. In a workspace, an admin turns it on in workspace permissions.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">If device code is not available to you, OpenAI documents two fallbacks. One forwards the fixed callback port, so here a tunnel does work: connect with <span class=\"highlighted\">ssh -L 1455:localhost:1455 dev@vps.example.com<\/span>, run <span class=\"highlighted\">codex login<\/span> on the VPS and open the printed URL on the laptop. The other copies a login made on the laptop, which works only if the laptop saved it to <span class=\"highlighted\">~\/.codex\/auth.json<\/span> rather than to an OS credential store:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>ssh dev@vps.example.com 'mkdir -p ~\/.codex'\nscp ~\/.codex\/auth.json dev@vps.example.com:~\/.codex\/auth.json\nssh dev@vps.example.com 'chmod 600 ~\/.codex\/auth.json'<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">For API billing, pipe the key in through stdin. A ChatGPT login draws on your plan, while a key switches Codex to standard API pricing:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>read -rs OPENAI_API_KEY &amp;&amp; export OPENAI_API_KEY\nprintenv OPENAI_API_KEY | codex login --with-api-key\nunset OPENAI_API_KEY<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">OpenAI says to treat <span class=\"highlighted\">~\/.codex\/auth.json<\/span> like a password, because it holds access tokens. The <span class=\"highlighted\">cli_auth_credentials_store<\/span> setting in <span class=\"highlighted\">~\/.codex\/config.toml<\/span> takes <span class=\"highlighted\">file<\/span>, <span class=\"highlighted\">keyring<\/span>, <span class=\"highlighted\">auto<\/span> or <span class=\"highlighted\">ephemeral<\/span>. With <span class=\"highlighted\">auto<\/span>, a server that has no OS credential store falls back to the file, so the 600 permissions and the firewall rule above matter for both CLIs.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">If you are setting up a remote dev box for this, size it for Anthropic&#8217;s 4 GB RAM minimum plus whatever runs beside Claude Code. Pick a plan of at least that size from the <strong><a href=\"https:\/\/hostry.com\/services\/vps\/\">Hostry VPS catalog, filtered by country and city<\/a><\/strong>; plans start at $6 a month at the time of writing, with Ubuntu 24.04 images and full root.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Related reading for other agents on the same server: how <strong><a href=\"https:\/\/hostry.com\/blog\/hermes-agent-vs-openclaw-vps\/\">Hermes Agent and OpenClaw compare as self-hosted agents<\/a><\/strong>.<\/p>\n<!-- AddThis Advanced Settings generic via filter on the_content --><!-- AddThis Share Buttons generic via filter on the_content -->","protected":false},"excerpt":{"rendered":"<p>Claude Code login over SSH fails in two ways: when the browser cannot reach the callback on the VPS, paste the code it shows; when the sign-in page re&#8230;<\/p>\n","protected":false},"author":8,"featured_media":2967,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[1,78],"tags":[],"class_list":["post-2976","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-virtual-private-server","category-apps"],"read_time":10,"_links":{"self":[{"href":"https:\/\/hostry.com\/blog\/wp-json\/wp\/v2\/posts\/2976","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/hostry.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/hostry.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/hostry.com\/blog\/wp-json\/wp\/v2\/users\/8"}],"replies":[{"embeddable":true,"href":"https:\/\/hostry.com\/blog\/wp-json\/wp\/v2\/comments?post=2976"}],"version-history":[{"count":2,"href":"https:\/\/hostry.com\/blog\/wp-json\/wp\/v2\/posts\/2976\/revisions"}],"predecessor-version":[{"id":2983,"href":"https:\/\/hostry.com\/blog\/wp-json\/wp\/v2\/posts\/2976\/revisions\/2983"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/hostry.com\/blog\/wp-json\/wp\/v2\/media\/2967"}],"wp:attachment":[{"href":"https:\/\/hostry.com\/blog\/wp-json\/wp\/v2\/media?parent=2976"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/hostry.com\/blog\/wp-json\/wp\/v2\/categories?post=2976"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/hostry.com\/blog\/wp-json\/wp\/v2\/tags?post=2976"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}