Skip to content

Commit 4a84d2a

Browse files
authored
Merge pull request #169 from underworldcode/bugfix/uw-claude-oauth-token
uw: add `claude-set-token` subcommand + dev-env activation for OAuth token
2 parents 49b7b17 + ac0949b commit 4a84d2a

3 files changed

Lines changed: 139 additions & 0 deletions

File tree

pixi.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,9 @@ anthropic = "*"
323323
sphinx-math-dollar = "*"
324324
sphinxcontrib-mermaid = "*"
325325

326+
[feature.dev.activation]
327+
scripts = ["scripts/activate-claude-auth.sh"]
328+
326329
[feature.dev.tasks]
327330
install-claude = "npm install -g @anthropic-ai/claude-code"
328331
claude = "claude"

scripts/activate-claude-auth.sh

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
#!/usr/bin/env bash
2+
# activate-claude-auth.sh — sourced by pixi on dev-environment activation.
3+
#
4+
# Auto-exports CLAUDE_CODE_OAUTH_TOKEN from a user-managed file if it
5+
# exists. Lets users skip the manual `export CLAUDE_CODE_OAUTH_TOKEN=...`
6+
# in shell rcs and avoids leaking the token outside dev pixi envs.
7+
#
8+
# Token file is written by `./uw claude-set-token` (mode 0600).
9+
# To remove: rm ~/.claude/uw-token
10+
11+
if [ -f "$HOME/.claude/uw-token" ]; then
12+
# Strip ALL whitespace (covers \r\n line endings, accidental leading
13+
# spaces, and any whitespace a manual editor leaves behind). $() alone
14+
# only trims trailing newlines, which is not enough here.
15+
_uw_claude_token="$(tr -d '[:space:]' < "$HOME/.claude/uw-token")"
16+
if [ -n "$_uw_claude_token" ]; then
17+
CLAUDE_CODE_OAUTH_TOKEN="$_uw_claude_token"
18+
export CLAUDE_CODE_OAUTH_TOKEN
19+
fi
20+
unset _uw_claude_token
21+
fi

uw

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -669,6 +669,117 @@ else:
669669
echo " Includes: git, gh, pixi, build tools, desktop notifications (macOS + Linux)"
670670
}
671671

672+
# Help for `./uw claude-set-token`
673+
claude_set_token_usage() {
674+
cat << 'EOF'
675+
./uw claude-set-token — store a Claude Code OAuth token for use in dev pixi envs.
676+
677+
Usage:
678+
./uw claude-set-token [TOKEN]
679+
./uw claude-set-token --help
680+
681+
TOKEN may be passed as an argument, piped on stdin, or entered at the
682+
interactive prompt (with input hidden). Tokens look like
683+
sk-ant-oat01-... and are obtained on a machine with a browser by
684+
running: claude setup-token (Pro/Max subscription required).
685+
686+
The token is written to ~/.claude/uw-token (mode 0600). The dev pixi
687+
env's activation script (scripts/activate-claude-auth.sh) auto-exports
688+
it as CLAUDE_CODE_OAUTH_TOKEN whenever you enter the env via
689+
./uw shell or pixi run -e <dev-env>. The token is NOT exported in
690+
non-dev envs or in shells outside pixi.
691+
692+
To remove the stored token: rm ~/.claude/uw-token
693+
EOF
694+
}
695+
696+
# Write a Claude Code OAuth token to ~/.claude/uw-token (mode 0600).
697+
# Token may come from $1, stdin (if piped), or an interactive prompt.
698+
run_claude_set_token() {
699+
if [ "$1" = "-h" ] || [ "$1" = "--help" ]; then
700+
claude_set_token_usage
701+
return 0
702+
fi
703+
704+
local token="$1"
705+
706+
if [ -z "$token" ]; then
707+
# `set -e` is in effect at script scope; without `|| true` a `read`
708+
# that hits EOF (empty pipe input, or Ctrl-D at the prompt) would
709+
# exit the whole script and bypass the "No token provided" handler.
710+
if [ ! -t 0 ]; then
711+
# stdin is a pipe — read one line
712+
read -r token || true
713+
else
714+
echo -e "${BOLD}Set Claude Code OAuth token${NC}"
715+
echo " Get one with 'claude setup-token' on a machine with a browser."
716+
echo " See './uw claude-set-token --help' for details."
717+
echo ""
718+
read -r -s -p "Token (input hidden): " token || true
719+
echo ""
720+
fi
721+
fi
722+
723+
# Strip whitespace (paste artifacts often include trailing newlines)
724+
token="$(printf '%s' "$token" | tr -d '[:space:]')"
725+
726+
if [ -z "$token" ]; then
727+
echo -e "${YELLOW}No token provided.${NC}" >&2
728+
return 1
729+
fi
730+
731+
if [[ "$token" != sk-ant-oat01-* ]]; then
732+
echo -e "${YELLOW}Token doesn't look like a Claude Code OAuth token (expected sk-ant-oat01-... prefix).${NC}" >&2
733+
echo " Get one with 'claude setup-token' on a machine with a browser." >&2
734+
return 1
735+
fi
736+
737+
mkdir -p "$HOME/.claude"
738+
chmod 700 "$HOME/.claude" 2>/dev/null || true
739+
740+
local token_file="$HOME/.claude/uw-token"
741+
742+
# Refuse to follow a symlink at the destination — a redirect through one
743+
# would clobber whatever it points at, regardless of how strict our umask is.
744+
if [ -L "$token_file" ]; then
745+
echo -e "${YELLOW}Refusing to write through symlink at $token_file${NC}" >&2
746+
echo " Remove or replace it manually if this is intentional." >&2
747+
return 1
748+
fi
749+
750+
if [ -f "$token_file" ]; then
751+
echo -e " ${YELLOW}Overwriting existing token at $token_file${NC}"
752+
fi
753+
754+
# Atomic replace: write to a sibling temp file (same filesystem), set
755+
# mode 0600, then `mv` into place. mktemp creates the temp file 0600
756+
# already; the umask 077 in the redirect subshell is belt-and-braces
757+
# against any weird default mask environments.
758+
local tmp_file
759+
tmp_file="$(mktemp "$HOME/.claude/.uw-token.XXXXXX")" || {
760+
echo -e "${YELLOW}Failed to create temp file in $HOME/.claude/${NC}" >&2
761+
return 1
762+
}
763+
(umask 077 && printf '%s' "$token" > "$tmp_file") || {
764+
rm -f "$tmp_file"
765+
echo -e "${YELLOW}Failed to write temp file $tmp_file${NC}" >&2
766+
return 1
767+
}
768+
chmod 600 "$tmp_file"
769+
mv -f "$tmp_file" "$token_file" || {
770+
rm -f "$tmp_file"
771+
echo -e "${YELLOW}Failed to move temp file into place${NC}" >&2
772+
return 1
773+
}
774+
775+
echo -e " ${GREEN}${NC} Token written to $token_file (mode 0600)"
776+
echo ""
777+
echo "Activate it by entering a dev pixi env:"
778+
echo " ./uw shell"
779+
echo ""
780+
echo "The dev-env activation script auto-exports CLAUDE_CODE_OAUTH_TOKEN."
781+
}
782+
672783
# Interactive setup wizard
673784
run_setup() {
674785
# Ensure pixi is available
@@ -972,6 +1083,7 @@ COMMANDS
9721083
set-env NAME Change environment directly
9731084
ai-tools Configure external AI instruction paths
9741085
claude-perms Configure Claude Code permissions (safe defaults)
1086+
claude-set-token Store Claude OAuth token for auto-export in dev envs (--help for details)
9751087
install-claude Install Claude Code CLI into the dev pixi env (run via ./uw claude)
9761088
9771089
Building:
@@ -1692,6 +1804,9 @@ case "${1:-}" in
16921804
claude-perms)
16931805
configure_claude_permissions "$(get_env)"
16941806
;;
1807+
claude-set-token)
1808+
run_claude_set_token "${2:-}"
1809+
;;
16951810
install-claude)
16961811
# The install-claude pixi task lives under [feature.dev.tasks], so it
16971812
# only resolves in dev-feature envs (dev, amr-dev, *-dev). Guard the

0 commit comments

Comments
 (0)