Skip to content

Instantly share code, notes, and snippets.

@lukehinds
Last active April 20, 2026 10:09
Show Gist options
  • Select an option

  • Save lukehinds/275325a85d5649f18cbc3a187e5a52d5 to your computer and use it in GitHub Desktop.

Select an option

Save lukehinds/275325a85d5649f18cbc3a187e5a52d5 to your computer and use it in GitHub Desktop.
#!/usr/bin/env bash
# Capture a real Claude auth/login/renewal failure without depending on other
# repo-local helper scripts.
#
# This script is intended to be publishable as a single standalone artifact.
set -euo pipefail
usage() {
cat <<'EOF'
Usage:
./capture-live-claude-auth-failure.sh [options]
Options:
--label TEXT
Human-friendly label for the capture bundle.
--nono-bin PATH
nono executable to use.
Default: `command -v nono`
--claude-bin PATH
Claude executable to use.
Default: `command -v claude`
--output-dir DIR
Parent directory for output bundles.
Default: ${TMPDIR:-/tmp}/nono-live-claude-auth-failure
--claude-config-dir DIR
Treat this as the active CLAUDE_CONFIG_DIR for snapshots and repro.
--note TEXT
Add a note to the bundle. May be repeated.
--help, -h
Show this help.
What it does:
1. Captures current environment/auth metadata
2. Runs one controlled startup probe under nono
3. Captures before/after auth-state and file snapshots
4. Archives the whole bundle as a tar.gz
When Claude launches:
- reproduce the auth failure once
- do not try multiple different fixes
- once the outcome is clear, exit Claude
EOF
}
fail() {
echo "Error: $*" >&2
exit 1
}
timestamp() {
date '+%Y-%m-%d %H:%M:%S'
}
log() {
printf '[%s] %s\n' "$(timestamp)" "$*" | tee -a "$RUN_LOG"
}
sanitize_label() {
printf '%s' "$1" | tr -cs 'A-Za-z0-9._-' '-'
}
detect_realpath() {
local path="$1"
if command -v realpath >/dev/null 2>&1; then
realpath "$path"
return
fi
python3 - <<'PY' "$path"
import os
import sys
print(os.path.realpath(sys.argv[1]))
PY
}
run_and_capture() {
local outfile="$1"
shift
{
printf '$'
for arg in "$@"; do
printf ' %q' "$arg"
done
printf '\n'
"$@" 2>&1
} >"$outfile"
}
run_interactive_and_capture() {
local outfile="$1"
shift
local cmd_file
cmd_file="$(mktemp "${TMPDIR:-/tmp}/nono-live-auth-cmd.XXXXXX")"
{
printf '#!/bin/sh\n'
printf 'exec'
for arg in "$@"; do
printf ' %q' "$arg"
done
printf '\n'
} >"$cmd_file"
chmod 700 "$cmd_file"
set +e
if command -v script >/dev/null 2>&1; then
script -q "$outfile" "$cmd_file"
local cmd_rc=$?
else
"$cmd_file" >"$outfile" 2>&1
local cmd_rc=$?
fi
set -e
printf '\n[exit-code] %s\n' "$cmd_rc" >>"$outfile"
rm -f "$cmd_file"
return "$cmd_rc"
}
write_header_file() {
local outfile="$1"
shift
printf '%s\n' "$@" >"$outfile"
}
is_truthy() {
local value
value="$(printf '%s' "${1:-}" | tr '[:upper:]' '[:lower:]')"
case "$value" in
1|true|yes|on)
return 0
;;
*)
return 1
;;
esac
}
is_macos() {
[[ "$(uname -s)" == "Darwin" ]]
}
hash_prefix() {
local input="$1"
if command -v shasum >/dev/null 2>&1; then
printf '%s' "$input" | shasum -a 256 | awk '{print substr($1, 1, 8)}'
return
fi
if command -v sha256sum >/dev/null 2>&1; then
printf '%s' "$input" | sha256sum | awk '{print substr($1, 1, 8)}'
return
fi
perl -MDigest::SHA=sha256_hex -e '
use strict;
use warnings;
local $/;
my $value = <STDIN>;
print substr(sha256_hex($value // q{}), 0, 8);
'
}
oauth_file_suffix() {
if [[ -n "${CLAUDE_CODE_CUSTOM_OAUTH_URL:-}" ]]; then
printf '%s' '-custom-oauth'
return
fi
if [[ "${USER_TYPE:-}" == "ant" ]]; then
if is_truthy "${USE_LOCAL_OAUTH:-}"; then
printf '%s' '-local-oauth'
return
fi
if is_truthy "${USE_STAGING_OAUTH:-}"; then
printf '%s' '-staging-oauth'
return
fi
fi
printf '%s' ''
}
get_username() {
if [[ -n "${USER:-}" ]]; then
printf '%s' "$USER"
return
fi
if id -un >/dev/null 2>&1; then
id -un
return
fi
printf '%s' 'claude-code-user'
}
global_config_path() {
if [[ -f "$LEGACY_CONFIG_PATH" ]]; then
printf '%s' "$LEGACY_CONFIG_PATH"
return
fi
if [[ "$CLAUDE_CONFIG_DIR_SET" -eq 1 ]]; then
printf '%s/.claude%s.json' "$RESOLVED_CONFIG_DIR" "$OAUTH_SUFFIX"
return
fi
printf '%s/.claude%s.json' "$HOME" "$OAUTH_SUFFIX"
}
keychain_service_name() {
local service_suffix="$1"
local dir_hash=""
if [[ "$CLAUDE_CONFIG_DIR_SET" -eq 1 ]]; then
dir_hash="-$(hash_prefix "$RESOLVED_CONFIG_DIR")"
fi
printf 'Claude Code%s%s%s' "$OAUTH_SUFFIX" "$service_suffix" "$dir_hash"
}
keychain_item_exists() {
local service_name="$1"
is_macos || return 1
security find-generic-password -a "$USERNAME" -w -s "$service_name" >/dev/null 2>&1
}
show_auth_state() {
echo "Mode: show"
echo "Config dir: $RESOLVED_CONFIG_DIR"
if [[ "$CLAUDE_CONFIG_DIR_SET" -eq 1 ]]; then
echo "CLAUDE_CONFIG_DIR semantics: explicit"
else
echo "CLAUDE_CONFIG_DIR semantics: default"
fi
echo "OAuth suffix: ${OAUTH_SUFFIX:-<prod>}"
echo "Credentials file: $CREDENTIALS_PATH"
if [[ -f "$CREDENTIALS_PATH" ]]; then
echo "Credentials file exists: yes"
else
echo "Credentials file exists: no"
fi
echo "Global config file: $GLOBAL_CONFIG_PATH"
if [[ -f "$GLOBAL_CONFIG_PATH" ]]; then
echo "Global config exists: yes"
else
echo "Global config exists: no"
fi
if is_macos; then
echo "OAuth keychain service: $OAUTH_KEYCHAIN_SERVICE"
if keychain_item_exists "$OAUTH_KEYCHAIN_SERVICE"; then
echo "OAuth keychain entry exists: yes"
else
echo "OAuth keychain entry exists: no"
fi
echo "Legacy API key keychain service: $API_KEY_KEYCHAIN_SERVICE"
if keychain_item_exists "$API_KEY_KEYCHAIN_SERVICE"; then
echo "Legacy API key keychain entry exists: yes"
else
echo "Legacy API key keychain entry exists: no"
fi
else
echo "Keychain inspection: skipped (non-macOS)"
fi
if [[ "$CLAUDE_CONFIG_DIR_SET" -eq 1 ]]; then
printf 'Run Claude with: CLAUDE_CONFIG_DIR=%q claude\n' "$RESOLVED_CONFIG_DIR"
else
echo "Run Claude with its default config dir."
fi
}
sha256_short() {
local path="$1"
if command -v shasum >/dev/null 2>&1; then
shasum -a 256 "$path" | awk '{print substr($1, 1, 16)}'
return
fi
if command -v sha256sum >/dev/null 2>&1; then
sha256sum "$path" | awk '{print substr($1, 1, 16)}'
return
fi
python3 - <<'PY' "$path"
import hashlib
import pathlib
import sys
path = pathlib.Path(sys.argv[1])
print(hashlib.sha256(path.read_bytes()).hexdigest()[:16])
PY
}
file_stat_line() {
local path="$1"
local kind size mtime hash
if [[ -L "$path" ]]; then
kind="symlink"
elif [[ -d "$path" ]]; then
kind="dir"
elif [[ -f "$path" ]]; then
kind="file"
else
printf 'missing|%s\n' "$path"
return
fi
if [[ "$kind" == "dir" ]]; then
size="-"
hash="-"
elif [[ "$kind" == "symlink" ]]; then
size="-"
hash="->$(readlink "$path")"
else
size="$(wc -c <"$path" | tr -d ' ')"
hash="$(sha256_short "$path")"
fi
if stat -f '%Sm' -t '%Y-%m-%dT%H:%M:%S%z' "$path" >/dev/null 2>&1; then
mtime="$(stat -f '%Sm' -t '%Y-%m-%dT%H:%M:%S%z' "$path")"
else
mtime="$(stat -c '%y' "$path" 2>/dev/null | awk '{print $1"T"$2}')"
fi
printf '%s|%s|%s|%s|%s\n' "$kind" "$path" "$size" "$mtime" "$hash"
}
collect_path_snapshot() {
local outfile="$1"
shift
: >"$outfile"
for target in "$@"; do
file_stat_line "$target" >>"$outfile"
done
}
capture_state_bundle() {
local phase_dir="$1"
show_auth_state >"$phase_dir/auth-state-show.txt"
collect_path_snapshot "$phase_dir/path-snapshot.txt" "${TRACKED_PATHS[@]}"
{
printf 'path|status\n'
for target in "${TRACKED_PATHS[@]}"; do
if [[ -e "$target" || -L "$target" ]]; then
printf '%s|present\n' "$target"
else
printf '%s|missing\n' "$target"
fi
done
} >"$phase_dir/presence.txt"
}
LABEL=""
OUTPUT_PARENT="${TMPDIR:-/tmp}/nono-live-claude-auth-failure"
CLAUDE_BIN="${CLAUDE_BIN:-$(command -v claude || true)}"
NONO_BIN="${NONO_BIN:-$(command -v nono || true)}"
CLAUDE_CONFIG_DIR_VALUE=""
CLAUDE_CONFIG_DIR_SET=0
NOTES=()
while [[ $# -gt 0 ]]; do
case "$1" in
--label)
[[ $# -ge 2 ]] || fail "--label requires a value"
LABEL="$2"
shift 2
;;
--nono-bin)
[[ $# -ge 2 ]] || fail "--nono-bin requires a value"
NONO_BIN="$2"
shift 2
;;
--claude-bin)
[[ $# -ge 2 ]] || fail "--claude-bin requires a value"
CLAUDE_BIN="$2"
shift 2
;;
--output-dir)
[[ $# -ge 2 ]] || fail "--output-dir requires a value"
OUTPUT_PARENT="$2"
shift 2
;;
--claude-config-dir)
[[ $# -ge 2 ]] || fail "--claude-config-dir requires a value"
CLAUDE_CONFIG_DIR_VALUE="$2"
CLAUDE_CONFIG_DIR_SET=1
shift 2
;;
--note)
[[ $# -ge 2 ]] || fail "--note requires a value"
NOTES+=("$2")
shift 2
;;
--help|-h)
usage
exit 0
;;
*)
fail "unknown argument: $1"
;;
esac
done
[[ -n "$CLAUDE_BIN" && -x "$CLAUDE_BIN" ]] || fail "claude binary not found or not executable"
[[ -n "$NONO_BIN" && -x "$NONO_BIN" ]] || fail "nono binary not found or not executable"
OS_NAME="$(uname -s)"
ARCH_NAME="$(uname -m)"
CLAUDE_REALPATH="$(detect_realpath "$CLAUDE_BIN")"
NONO_REALPATH="$(detect_realpath "$NONO_BIN")"
if [[ "$CLAUDE_CONFIG_DIR_SET" -eq 1 ]]; then
CONFIG_MODE="explicit"
RESOLVED_CONFIG_DIR="$CLAUDE_CONFIG_DIR_VALUE"
else
CONFIG_MODE="default"
RESOLVED_CONFIG_DIR="$HOME/.claude"
fi
RESOLVED_LOCK_DIR="${RESOLVED_CONFIG_DIR}.lock"
OAUTH_SUFFIX="$(oauth_file_suffix)"
CREDENTIALS_PATH="$RESOLVED_CONFIG_DIR/.credentials.json"
LEGACY_CONFIG_PATH="$RESOLVED_CONFIG_DIR/.config.json"
GLOBAL_CONFIG_PATH="$(global_config_path)"
USERNAME="$(get_username)"
OAUTH_KEYCHAIN_SERVICE="$(keychain_service_name '-credentials')"
API_KEY_KEYCHAIN_SERVICE="$(keychain_service_name '')"
TRACKED_PATHS=(
"$RESOLVED_CONFIG_DIR"
"$RESOLVED_LOCK_DIR"
"$CREDENTIALS_PATH"
"$GLOBAL_CONFIG_PATH"
"$HOME/.claude.json"
"$HOME/.claude.json.lock"
"$HOME/.cache/claude"
"$HOME/.cache/claude-cli-nodejs"
)
mkdir -p "$OUTPUT_PARENT"
STAMP="$(date '+%Y%m%d-%H%M%S')"
RUN_NAME="$STAMP"
if [[ -n "$LABEL" ]]; then
RUN_NAME="${RUN_NAME}-$(sanitize_label "$LABEL")"
else
RUN_NAME="${RUN_NAME}-live-auth-failure"
fi
RUN_DIR="$OUTPUT_PARENT/$RUN_NAME"
mkdir -p "$RUN_DIR"
RUN_LOG="$RUN_DIR/run.log"
ENV_DIR="$RUN_DIR/env-capture"
REPRO_DIR="$RUN_DIR/repro"
BEFORE_DIR="$REPRO_DIR/before"
AFTER_DIR="$REPRO_DIR/after"
mkdir -p "$ENV_DIR" "$REPRO_DIR" "$BEFORE_DIR" "$AFTER_DIR"
if ((${#NOTES[@]} > 0)); then
write_header_file "$RUN_DIR/notes.txt" "${NOTES[@]}"
else
: >"$RUN_DIR/notes.txt"
fi
{
echo "timestamp=$STAMP"
echo "label=$LABEL"
echo "os=$OS_NAME"
echo "arch=$ARCH_NAME"
echo "claude_bin=$CLAUDE_BIN"
echo "claude_realpath=$CLAUDE_REALPATH"
echo "nono_bin=$NONO_BIN"
echo "nono_realpath=$NONO_REALPATH"
echo "config_mode=$CONFIG_MODE"
echo "claude_config_dir=$RESOLVED_CONFIG_DIR"
echo "run_dir=$RUN_DIR"
} >"$RUN_DIR/metadata.env"
cat >"$RUN_DIR/README.md" <<EOF
# Live Claude Auth Failure Capture
- Timestamp: $STAMP
- Label: ${LABEL:-<unset>}
- OS: $OS_NAME
- Arch: $ARCH_NAME
- Claude binary: \`$CLAUDE_BIN\`
- Claude real path: \`$CLAUDE_REALPATH\`
- nono binary: \`$NONO_BIN\`
- nono real path: \`$NONO_REALPATH\`
- Config mode: $CONFIG_MODE
- Claude config dir: \`$RESOLVED_CONFIG_DIR\`
- Run directory: \`$RUN_DIR\`
## Files
- \`metadata.env\`
- \`notes.txt\`
- \`run.log\`
- \`env-capture/\` environment and auth metadata
- \`repro/\` controlled in-sandbox repro attempt
- \`NEXT_STEPS.txt\`
EOF
log "Capturing current environment state"
run_and_capture "$ENV_DIR/system.txt" /bin/sh -c '
uname -a
echo
sw_vers 2>/dev/null || true
echo
system_profiler SPSoftwareDataType 2>/dev/null || true
'
run_and_capture "$ENV_DIR/claude-version.txt" /bin/sh -c "
printf 'command -v claude: %s\n' \"\$(command -v claude || true)\"
printf 'claude bin argument: %s\n' \"$CLAUDE_BIN\"
printf 'claude real path: %s\n' \"$CLAUDE_REALPATH\"
echo
\"$CLAUDE_BIN\" --version || true
"
run_and_capture "$ENV_DIR/nono-version.txt" /bin/sh -c "
printf 'command -v nono: %s\n' \"\$(command -v nono || true)\"
printf 'nono bin argument: %s\n' \"$NONO_BIN\"
printf 'nono real path: %s\n' \"$NONO_REALPATH\"
echo
\"$NONO_BIN\" --version || true
"
run_and_capture "$ENV_DIR/claude-paths.txt" /bin/sh -c "
which -a claude || true
echo
ls -l \"$CLAUDE_BIN\" || true
echo
ls -ld \"$(dirname "$CLAUDE_BIN")\" || true
echo
ls -ld \"$HOME/.local\" \"$HOME/.local/bin\" \"$HOME/.local/share\" \"$HOME/.local/share/claude\" 2>/dev/null || true
echo
ls -ld \"$HOME/.claude\" \"$HOME/.claude.lock\" \"$HOME/.claude.json\" \"$HOME/.claude.json.lock\" 2>/dev/null || true
"
run_and_capture "$ENV_DIR/package-manager-hints.txt" /bin/sh -c '
npm list -g --depth=0 2>/dev/null | grep -i claude || true
echo
brew list --versions 2>/dev/null | grep -i claude || true
echo
python3 -m pip list 2>/dev/null | grep -i claude || true
'
run_and_capture "$ENV_DIR/env-vars.txt" /bin/sh -c '
env | sort | grep -E "^(CLAUDE|ANTHROPIC|NONO|CARGO|HOME|PATH|USER|SHELL|TMPDIR|XDG_)=" || true
'
show_auth_state >"$ENV_DIR/auth-state-show.txt"
startup_env=(env)
if [[ "$CLAUDE_CONFIG_DIR_SET" -eq 1 ]]; then
startup_env+=(CLAUDE_CONFIG_DIR="$RESOLVED_CONFIG_DIR")
fi
set +e
run_interactive_and_capture \
"$ENV_DIR/startup-probe.txt" \
"${startup_env[@]}" \
"$NONO_BIN" run --profile claude-code --allow-cwd -- "$CLAUDE_BIN"
startup_probe_rc=$?
set -e
printf '%s\n' "$startup_probe_rc" >"$ENV_DIR/startup-probe-exit-code.txt"
if [[ "$startup_probe_rc" -ne 0 ]]; then
log "Startup probe exited with status $startup_probe_rc; continuing with full capture"
fi
log "Capturing before snapshot"
capture_state_bundle "$BEFORE_DIR"
OPEN_PROBE_DIR=""
OPEN_WRAPPER_LOG=""
cleanup() {
if [[ -n "$OPEN_PROBE_DIR" && -d "$OPEN_PROBE_DIR" ]]; then
rm -rf "$OPEN_PROBE_DIR"
fi
}
trap cleanup EXIT
if is_macos; then
OPEN_PROBE_DIR="$(mktemp -d "${TMPDIR:-/tmp}/nono-claude-open-probe.XXXXXX")"
OPEN_WRAPPER_LOG="$REPRO_DIR/open-wrapper.log"
: >"$OPEN_WRAPPER_LOG"
chmod 600 "$OPEN_WRAPPER_LOG"
log "Installing macOS PATH open probe wrapper"
cat >"$OPEN_PROBE_DIR/open" <<'EOF'
#!/bin/sh
log_file="${NONO_OPEN_WRAPPER_LOG:?}"
{
printf 'pid=%s argc=%s\n' "$$" "$#"
i=1
for arg in "$@"; do
printf 'arg[%s]=%s\n' "$i" "$arg"
i=$((i + 1))
done
printf '\n'
} >>"$log_file"
exec /usr/bin/open "$@"
EOF
chmod 755 "$OPEN_PROBE_DIR/open"
fi
nono_args=(run --profile claude-code --allow-cwd)
if [[ "$CLAUDE_CONFIG_DIR_SET" -eq 1 ]]; then
nono_args+=(--allow "$RESOLVED_CONFIG_DIR" --allow "$RESOLVED_LOCK_DIR")
fi
nono_args+=(--)
launch_env=(env)
if [[ "$CLAUDE_CONFIG_DIR_SET" -eq 1 ]]; then
launch_env+=(CLAUDE_CONFIG_DIR="$RESOLVED_CONFIG_DIR")
fi
if [[ -n "$OPEN_PROBE_DIR" ]]; then
launch_env+=(PATH="$OPEN_PROBE_DIR:$PATH")
launch_env+=(NONO_OPEN_WRAPPER_LOG="$OPEN_WRAPPER_LOG")
fi
log "Launching Claude under nono"
log "Report dir: $REPRO_DIR"
log "Inside Claude, reproduce the auth failure once, then exit."
set +e
run_interactive_and_capture \
"$REPRO_DIR/claude-session.log" \
"${launch_env[@]}" \
"$NONO_BIN" "${nono_args[@]}" "$CLAUDE_BIN"
launch_rc=$?
set -e
printf '%s\n' "$launch_rc" >"$REPRO_DIR/launch-exit-code.txt"
log "Claude session exited with status $launch_rc"
log "Capturing after snapshot"
capture_state_bundle "$AFTER_DIR"
if [[ -f "$OPEN_WRAPPER_LOG" ]]; then
if [[ -s "$OPEN_WRAPPER_LOG" ]]; then
log "Open probe captured PATH-resolved open invocations"
printf 'hit\n' >"$REPRO_DIR/open-probe-status.txt"
else
log "Open probe did not capture any PATH-resolved open invocations"
printf 'no-hit\n' >"$REPRO_DIR/open-probe-status.txt"
fi
fi
if diff -u "$BEFORE_DIR/path-snapshot.txt" "$AFTER_DIR/path-snapshot.txt" \
>"$REPRO_DIR/path-snapshot.diff"; then
log "Tracked file snapshot unchanged"
else
log "Tracked file snapshot changed; see path-snapshot.diff"
fi
login_observed="no"
if grep -Eq "Login successful|Not logged in|Run /login|Select login method" \
"$REPRO_DIR/claude-session.log" 2>/dev/null; then
login_observed="yes"
fi
open_probe_status="not-run"
if [[ -f "$REPRO_DIR/open-probe-status.txt" ]]; then
open_probe_status="$(tr -d '\n' <"$REPRO_DIR/open-probe-status.txt")"
fi
path_snapshot_changed="no"
if [[ -s "$REPRO_DIR/path-snapshot.diff" ]]; then
path_snapshot_changed="yes"
fi
cat >"$REPRO_DIR/summary.env" <<EOF
launch_mode=profile
launch_exit_code=$launch_rc
login_observed=$login_observed
open_probe_status=$open_probe_status
path_snapshot_changed=$path_snapshot_changed
report_dir=$REPRO_DIR
EOF
cat >"$REPRO_DIR/README.md" <<EOF
# Claude Auth Repro Run
- Timestamp: $STAMP
- OS: $OS_NAME
- Launch mode: profile
- Claude binary: $CLAUDE_BIN
- nono binary: $NONO_BIN
- Claude config dir: $RESOLVED_CONFIG_DIR
- Report directory: $REPRO_DIR
- Claude exit code: $launch_rc
## Files
- \`claude-session.log\` full nono + Claude terminal log
- \`before/\` auth-state and file snapshots before launch
- \`after/\` auth-state and file snapshots after launch
- \`path-snapshot.diff\` diff of tracked file metadata before vs after
- \`open-wrapper.log\` macOS PATH-open probe output, when enabled
- \`summary.env\` compact outcome summary
EOF
ARCHIVE_PATH="${RUN_DIR}.tar.gz"
tar -C "$(dirname "$RUN_DIR")" -czf "$ARCHIVE_PATH" "$(basename "$RUN_DIR")"
cat >"$RUN_DIR/NEXT_STEPS.txt" <<EOF
Capture complete.
Primary artifacts:
- Directory: $RUN_DIR
- Archive: $ARCHIVE_PATH
Send the archive path or the archive itself for analysis.
Do not manually retype file contents from the bundle.
EOF
cat <<EOF
Live auth failure capture complete.
Run directory: $RUN_DIR
Archive: $ARCHIVE_PATH
EOF
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment