Claude Code: sign in without a browser (credentials)
Drops a ready-made Claude Code login into a workspace so you don't have to authenticate in every new container. Supply it either way: **paste** the contents of `~/.claude/.credentials.json` from a machine where you're signed in (the whole file, `{` to `}`), or **point at a file** — back a secret with `/home/dev/.claude/.credentials.json` in a workspace that is already signed in, and pick that secret. It is read fresh on every run, and Claude Code rewrites that file as its tokens roll over, so a file-backed login doesn't go stale. The value lands in `~/.claude/.credentials.json` (mode 600), and `hasCompletedOnboarding` is set in `~/.claude.json` to skip the first-run prompt. An existing credentials file is backed up to `.credentials.json.bak`; `jq` is installed when possible, and without it an existing `~/.claude.json` isn't merged. After it runs: start `claude` — it should come up already signed in.
What it asks for
- Claude Code credentials JSONrequired
- The full contents of ~/.claude/.credentials.json from a machine where you're signed in (`cat ~/.claude/.credentials.json`) — or a secret backed by that file in a workspace that is already signed in, which stays current without re-pasting. Redacted from run logs and never persisted into the container's env profile.
- Overwrite existing login
- Choose 'no' to keep a login that is already in the container and only do the rest of the setup.
What it runs
The whole script, exactly as it will execute in your workspace. Nothing is hidden — read it before you run it.
#!/usr/bin/env sh
set -eu
umask 077 # anything this script creates is owner-only by default
log() { printf '[claude-setup] %s\n' "$*"; }
warn() { printf '[claude-setup] WARNING: %s\n' "$*" >&2; }
die() { printf '[claude-setup] ERROR: %s\n' "$*" >&2; exit 1; }
CREDS_JSON="${CLAUDE_CODE_CREDENTIALS_JSON:-}"
OVERWRITE="${OVERWRITE_EXISTING_CREDENTIALS:-yes}"
[ -n "$CREDS_JSON" ] || die "no credentials JSON was supplied"
command -v claude >/dev/null 2>&1 \
|| die "the 'claude' CLI is not on PATH — run this in a workspace that has Claude Code installed"
if [ "$(id -u)" -eq 0 ]; then SUDO=""
elif command -v sudo >/dev/null 2>&1; then SUDO="sudo -n"
else SUDO=""
fi
# jq is optional: it only merges an existing ~/.claude.json and sanity-checks the
# credentials. Never fail the run over it — the login matters more.
# Note: this script body is piped to `sh -s` on stdin, so stdin IS the remaining
# script. Every installer gets </dev/null or a prompt would eat the script.
if ! command -v jq >/dev/null 2>&1; then
log "jq not found; attempting install (optional)"
export DEBIAN_FRONTEND=noninteractive
if command -v apt-get >/dev/null 2>&1; then $SUDO apt-get update -qq </dev/null && $SUDO apt-get install -y -qq jq </dev/null || true
elif command -v dnf >/dev/null 2>&1; then $SUDO dnf install -y jq </dev/null || true
elif command -v yum >/dev/null 2>&1; then $SUDO yum install -y jq </dev/null || true
elif command -v apk >/dev/null 2>&1; then $SUDO apk add --no-cache jq </dev/null || true
elif command -v pacman >/dev/null 2>&1; then $SUDO pacman -Sy --noconfirm jq </dev/null || true
elif command -v zypper >/dev/null 2>&1; then $SUDO zypper install -y jq </dev/null || true
elif command -v brew >/dev/null 2>&1; then brew install jq </dev/null || true
fi
command -v jq >/dev/null 2>&1 || warn "continuing without jq"
fi
# -- credentials ---------------------------------------------------------------
mkdir -p "$HOME/.claude"
CREDS="$HOME/.claude/.credentials.json"
if command -v jq >/dev/null 2>&1; then
printf '%s' "$CREDS_JSON" | jq empty >/dev/null 2>&1 \
|| die "the supplied value is not valid JSON — paste the whole file contents, from '{' to '}'"
fi
if [ -f "$CREDS" ] && [ "$OVERWRITE" != "yes" ]; then
log "$CREDS already exists and overwrite is off — leaving it alone"
else
if [ -f "$CREDS" ]; then
cp -p "$CREDS" "$CREDS.bak"
log "backed up previous credentials to $CREDS.bak"
fi
# printf, not a heredoc: the value must land verbatim, with no shell expansion.
printf '%s\n' "$CREDS_JSON" > "$CREDS"
chmod 600 "$CREDS"
log "wrote $CREDS (mode 600)"
fi
# -- skip the onboarding prompt ------------------------------------------------
CLAUDE_JSON="$HOME/.claude.json"
if [ ! -f "$CLAUDE_JSON" ]; then
printf '%s\n' '{"hasCompletedOnboarding":true}' > "$CLAUDE_JSON"
log "created $CLAUDE_JSON"
elif command -v jq >/dev/null 2>&1; then
tmp="$CLAUDE_JSON.devgrail.tmp" # same directory: atomic rename, no cross-device copy
trap 'rm -f "$tmp"' EXIT
jq '.hasCompletedOnboarding = true' "$CLAUDE_JSON" > "$tmp" \
|| die "jq failed to process $CLAUDE_JSON"
[ -s "$tmp" ] || die "jq produced empty output for $CLAUDE_JSON — aborting to avoid data loss"
cp -p "$CLAUDE_JSON" "$CLAUDE_JSON.bak"
mv "$tmp" "$CLAUDE_JSON"
trap - EXIT
log "updated $CLAUDE_JSON (backup: $CLAUDE_JSON.bak)"
else
warn "jq unavailable and $CLAUDE_JSON exists — left untouched; if Claude Code shows the onboarding prompt, set \"hasCompletedOnboarding\": true there by hand"
fi
log "done. Run 'claude' — it should start without asking you to log in."
How to get it
Open your DevGrail workspace, go to Scripts → Official scripts, and choose Download. It is copied into your own library, where you can edit it, run it, or set it as a workspace default — the copy is yours, and later changes here do not touch it.