Skip to content

Commit 4fe3d7c

Browse files
akshithgclaude
andcommitted
fix: improve error handling in onboarding bypass
- Handle timeout as expected (claude -p writes config before API call) - Catch FileNotFoundError/OSError if claude is not installed - Check returncode explicitly instead of dead CalledProcessError catch - Guard on ~/.claude.json existence before writing onboarding flag - Replace contextlib.suppress with explicit try/except that logs - Update module docstring and README wording Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent b98bd8a commit 4fe3d7c

2 files changed

Lines changed: 46 additions & 15 deletions

File tree

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -116,15 +116,15 @@ claude # Ready to work
116116

117117
## Token-Based Auth (Headless)
118118

119-
For non-interactive setups (CI, headless servers, or skipping the login wizard):
119+
For headless servers or to skip the interactive login wizard:
120120

121121
```bash
122122
claude setup-token # run on host, one-time
123123
export CLAUDE_CODE_OAUTH_TOKEN=sk-ant-oat01-...
124124
devc rebuild # rebuilds with token
125125
```
126126

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.
127+
The token is forwarded into the container. On each container creation, `post_install.py` runs a one-shot auth handshake so `claude` starts without the login wizard.
128128

129129
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)).
130130

post_install.py

Lines changed: 44 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
"""Post-install configuration for Claude Code devcontainer.
33
44
Runs on container creation to set up:
5+
- Onboarding bypass (when CLAUDE_CODE_OAUTH_TOKEN is set)
56
- Claude settings (bypassPermissions mode)
67
- Tmux configuration (200k history, mouse support)
78
- Directory ownership fixes for mounted volumes
@@ -16,10 +17,14 @@
1617

1718

1819
def setup_onboarding_bypass():
19-
"""Bypass the interactive onboarding wizard using CLAUDE_CODE_OAUTH_TOKEN.
20+
"""Bypass the interactive onboarding wizard when CLAUDE_CODE_OAUTH_TOKEN is set.
21+
22+
Runs `claude -p` to seed ~/.claude.json with auth state. The subprocess
23+
writes the config file during startup before the API call completes, so
24+
a timeout is expected and acceptable. After the subprocess finishes (or
25+
times out), we check whether ~/.claude.json was populated and only then
26+
set hasCompletedOnboarding.
2027
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.
2328
Workaround for https://github.com/anthropics/claude-code/issues/8938.
2429
"""
2530
token = os.environ.get("CLAUDE_CODE_OAUTH_TOKEN", "").strip()
@@ -32,25 +37,51 @@ def setup_onboarding_bypass():
3237

3338
claude_json = Path.home() / ".claude.json"
3439

35-
# Run a one-shot claude -p to trigger non-interactive auth
3640
print("[post_install] Running claude -p to populate auth state...", file=sys.stderr)
3741
try:
38-
subprocess.run(
42+
result = subprocess.run(
3943
["claude", "-p", "ok"],
4044
capture_output=True,
4145
text=True,
4246
timeout=30,
4347
)
44-
except (subprocess.CalledProcessError, subprocess.TimeoutExpired) as e:
45-
print(f"[post_install] Warning: claude -p failed: {e}", file=sys.stderr)
48+
if result.returncode != 0:
49+
print(
50+
f"[post_install] claude -p exited {result.returncode}: "
51+
f"{result.stderr.strip()}",
52+
file=sys.stderr,
53+
)
54+
except subprocess.TimeoutExpired:
55+
print(
56+
"[post_install] claude -p timed out (expected on cold start)",
57+
file=sys.stderr,
58+
)
59+
except (FileNotFoundError, OSError) as e:
60+
print(
61+
f"[post_install] Warning: could not run claude ({e}) — "
62+
"onboarding bypass skipped",
63+
file=sys.stderr,
64+
)
65+
return
4666

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())
67+
if not claude_json.exists():
68+
print(
69+
f"[post_install] Warning: {claude_json} not created by claude -p — "
70+
"onboarding bypass skipped",
71+
file=sys.stderr,
72+
)
73+
return
74+
75+
config: dict = {}
76+
try:
77+
config = json.loads(claude_json.read_text())
78+
except json.JSONDecodeError as e:
79+
print(
80+
f"[post_install] Warning: {claude_json} has invalid JSON ({e}), "
81+
"starting fresh",
82+
file=sys.stderr,
83+
)
5284

53-
# Mark onboarding as complete
5485
config["hasCompletedOnboarding"] = True
5586

5687
claude_json.write_text(json.dumps(config, indent=2) + "\n", encoding="utf-8")

0 commit comments

Comments
 (0)