@@ -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
673784run_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