Skip to content

Commit f477ed2

Browse files
akshithgclaude
andcommitted
feat: add non-interactive auth via CLAUDE_CODE_OAUTH_TOKEN
Bypass the interactive onboarding wizard when CLAUDE_CODE_OAUTH_TOKEN is set. On container create, post_install.py runs `claude -p` to populate auth state and sets hasCompletedOnboarding so the TUI starts without the login wizard. Workaround for anthropics/claude-code#8938. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 6dc4b4f commit f477ed2

3 files changed

Lines changed: 73 additions & 4 deletions

File tree

README.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,22 @@ cd client-repo-1
114114
claude # Ready to work
115115
```
116116

117+
## Token-Based Auth (Headless)
118+
119+
For non-interactive setups (CI, headless servers, or skipping the login wizard):
120+
121+
```bash
122+
claude setup-token # run on host, one-time
123+
export CLAUDE_CODE_OAUTH_TOKEN=sk-ant-oat01-...
124+
devc rebuild # rebuilds with token
125+
```
126+
127+
The token is forwarded into the container. On first create, `post_install.py` runs a one-shot auth handshake so `claude` starts without the login wizard.
128+
129+
This works around Claude Code's interactive onboarding wizard always showing in containers, even with valid credentials ([#8938](https://github.com/anthropics/claude-code/issues/8938)).
130+
131+
If you don't set a token, the interactive login flow works as before.
132+
117133
## CLI Helper Commands
118134

119135
```

devcontainer.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,9 @@
6161
"NPM_CONFIG_UPDATE_NOTIFIER": "false",
6262
"NPM_CONFIG_MINIMUM_RELEASE_AGE": "1440",
6363
"PYTHONDONTWRITEBYTECODE": "1",
64-
"PIP_DISABLE_PIP_VERSION_CHECK": "1"
64+
"PIP_DISABLE_PIP_VERSION_CHECK": "1",
65+
"CLAUDE_CODE_OAUTH_TOKEN": "${localEnv:CLAUDE_CODE_OAUTH_TOKEN:}",
66+
"ANTHROPIC_API_KEY": "${localEnv:ANTHROPIC_API_KEY:}"
6567
},
6668
"initializeCommand": "test -f \"$HOME/.gitconfig\" || touch \"$HOME/.gitconfig\"",
6769
"workspaceMount": "source=${localWorkspaceFolder},target=/workspace,type=bind,consistency=delegated",

post_install.py

Lines changed: 54 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,50 @@
1515
from pathlib import Path
1616

1717

18+
def setup_onboarding_bypass():
19+
"""Bypass the interactive onboarding wizard using CLAUDE_CODE_OAUTH_TOKEN.
20+
21+
When a token is set, runs a one-shot `claude -p` to populate auth state,
22+
then marks onboarding as complete so the TUI skips the login wizard.
23+
Workaround for https://github.com/anthropics/claude-code/issues/8938.
24+
"""
25+
token = os.environ.get("CLAUDE_CODE_OAUTH_TOKEN", "").strip()
26+
if not token:
27+
print(
28+
"[post_install] No CLAUDE_CODE_OAUTH_TOKEN set, skipping onboarding bypass",
29+
file=sys.stderr,
30+
)
31+
return
32+
33+
claude_json = Path.home() / ".claude.json"
34+
35+
# Run a one-shot claude -p to trigger non-interactive auth
36+
print("[post_install] Running claude -p to populate auth state...", file=sys.stderr)
37+
try:
38+
subprocess.run(
39+
["claude", "-p", "ok"],
40+
capture_output=True,
41+
text=True,
42+
timeout=30,
43+
)
44+
except (subprocess.CalledProcessError, subprocess.TimeoutExpired) as e:
45+
print(f"[post_install] Warning: claude -p failed: {e}", file=sys.stderr)
46+
47+
# Read existing ~/.claude.json or start fresh
48+
config = {}
49+
if claude_json.exists():
50+
with contextlib.suppress(json.JSONDecodeError):
51+
config = json.loads(claude_json.read_text())
52+
53+
# Mark onboarding as complete
54+
config["hasCompletedOnboarding"] = True
55+
56+
claude_json.write_text(json.dumps(config, indent=2) + "\n", encoding="utf-8")
57+
print(
58+
f"[post_install] Onboarding bypass configured: {claude_json}", file=sys.stderr
59+
)
60+
61+
1862
def setup_claude_settings():
1963
"""Configure Claude Code with bypassPermissions enabled."""
2064
claude_dir = Path.home() / ".claude"
@@ -34,7 +78,9 @@ def setup_claude_settings():
3478
settings["permissions"]["defaultMode"] = "bypassPermissions"
3579

3680
settings_file.write_text(json.dumps(settings, indent=2) + "\n", encoding="utf-8")
37-
print(f"[post_install] Claude settings configured: {settings_file}", file=sys.stderr)
81+
print(
82+
f"[post_install] Claude settings configured: {settings_file}", file=sys.stderr
83+
)
3884

3985

4086
def setup_tmux_config():
@@ -106,7 +152,9 @@ def fix_directory_ownership():
106152
check=True,
107153
capture_output=True,
108154
)
109-
print(f"[post_install] Fixed ownership: {dir_path}", file=sys.stderr)
155+
print(
156+
f"[post_install] Fixed ownership: {dir_path}", file=sys.stderr
157+
)
110158
except (PermissionError, subprocess.CalledProcessError) as e:
111159
print(
112160
f"[post_install] Warning: Could not fix ownership of {dir_path}: {e}",
@@ -204,13 +252,16 @@ def setup_global_gitignore():
204252
program = /usr/bin/ssh-keygen
205253
"""
206254
local_gitconfig.write_text(local_config, encoding="utf-8")
207-
print(f"[post_install] Local git config created: {local_gitconfig}", file=sys.stderr)
255+
print(
256+
f"[post_install] Local git config created: {local_gitconfig}", file=sys.stderr
257+
)
208258

209259

210260
def main():
211261
"""Run all post-install configuration."""
212262
print("[post_install] Starting post-install configuration...", file=sys.stderr)
213263

264+
setup_onboarding_bypass()
214265
setup_claude_settings()
215266
setup_tmux_config()
216267
fix_directory_ownership()

0 commit comments

Comments
 (0)