Files
orca/src/shared/claude-shell-function.ts
T
Wooseong KimandNeil 21ff9b8da2 fix(claude): exec Claude from the profile subshell so it leads the foreground group (#40962)
The Orca claude shell function launched Claude inside a ( ... ) subshell.
Bash can keep that subshell as the pane's foreground process, so Orca saw
"bash" instead of Claude. The subshell now unsets the claude function and
execs the binary, leaving Claude as a direct child of the pane shell.

Co-authored-by: Neil <neil@stably.ai>
2026-11-11 03:07:38 -08:00

114 lines
6.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/** Re-read account selection on every launch in Orca-routed panes with a real claude binary. Missing/empty pointer restores the user’s config; another pane override wins. Create account homes without a login silently and preserve Anthropic auth. Refuse nonabsolute pointers rather than run under the wrong account. */
export function getPosixClaudeShellFunction(): string {
return `__orca_claude_binary="$(unalias claude 2>/dev/null || :; command -v claude 2>/dev/null || :)"
# Why typeset -f: a user's own claude function wins; command -v prints its bare name, which -x tests against the cwd.
if ! typeset -f claude >/dev/null 2>&1 && [[ -n "\${ORCA_CLAUDE_PROFILE_POINTER:-}" && -n "\${__orca_claude_binary:-}" && -x "\${__orca_claude_binary}" ]]; then
function claude {
local __orca_claude_home __orca_claude_pointer="\${ORCA_CLAUDE_PROFILE_POINTER:-}"
# Why: a WSL pane's pointer is relative to the guest home, which the host cannot know at spawn.
case "$__orca_claude_pointer" in '~/'*) __orca_claude_pointer="\${HOME:-}/\${__orca_claude_pointer#??}" ;; esac
__orca_claude_home="$(cat "$__orca_claude_pointer" 2>/dev/null || :)"
if [ -n "\${CLAUDE_CONFIG_DIR:-}" ] && [ "$CLAUDE_CONFIG_DIR" != "\${ORCA_CLAUDE_INJECTED_CONFIG_DIR:-}" ] &&
{ [ -z "\${ORCA_CLAUDE_USER_CONFIG_DIR:-}" ] || [ "\${CLAUDE_CONFIG_DIR%/}" != "\${ORCA_CLAUDE_USER_CONFIG_DIR%/}" ]; }; then
command claude "$@"; return
fi
# Why exec: bash can keep the subshell leading Claude's foreground group; zsh exec runs functions, hence unset -f.
# Why: Orca's value replaced the user's own at spawn, so System default restores theirs.
if [ -z "$__orca_claude_home" ] && [ -n "\${CLAUDE_CONFIG_DIR:-}" ] && [ -n "\${ORCA_CLAUDE_USER_CONFIG_DIR:-}" ]; then
( unset ORCA_CLAUDE_INJECTED_CONFIG_DIR; export CLAUDE_CONFIG_DIR="$ORCA_CLAUDE_USER_CONFIG_DIR"; unset -f claude; exec claude "$@" ); return
fi
if [ -z "$__orca_claude_home" ]; then
( unset CLAUDE_CONFIG_DIR ORCA_CLAUDE_INJECTED_CONFIG_DIR; unset -f claude; exec claude "$@" ); return
fi
case "$__orca_claude_home" in /*|[A-Za-z]:*) ;; *) return 1 ;; esac
# Why -m 700: matches the folder Orca's setup creates; a credentials folder stays private.
[ -d "$__orca_claude_home" ] || mkdir -p -m 700 -- "$__orca_claude_home" 2>/dev/null
( export CLAUDE_CONFIG_DIR="$__orca_claude_home" ORCA_CLAUDE_INJECTED_CONFIG_DIR="$__orca_claude_home"; unset -f claude; exec claude "$@" )
}
fi
unset __orca_claude_binary
`
}
/** Leading newline separates the preceding codex fragment, which has no trailing newline. */
export function getFishClaudeShellFunction(): string {
return `
set -l __orca_claude_type (type -t claude 2>/dev/null)
if test -n "$ORCA_CLAUDE_PROFILE_POINTER"; and test "$__orca_claude_type" = file
function claude
# Why: a WSL pane's pointer is relative to the guest home, which the host cannot know at spawn.
set -l pointer (string replace -r '^~/' "$HOME/" -- "$ORCA_CLAUDE_PROFILE_POINTER")
set -l profile (cat "$pointer" 2>/dev/null)
set -l own (string trim -r -c / -- "$ORCA_CLAUDE_USER_CONFIG_DIR")
set -l current (string trim -r -c / -- "$CLAUDE_CONFIG_DIR")
if test -n "$CLAUDE_CONFIG_DIR"; and test "$CLAUDE_CONFIG_DIR" != "$ORCA_CLAUDE_INJECTED_CONFIG_DIR"; and begin; test -z "$own"; or test "$current" != "$own"; end
command claude $argv
return $status
end
if test -z "$profile"; and test -n "$CLAUDE_CONFIG_DIR"; and test -n "$ORCA_CLAUDE_USER_CONFIG_DIR"
env -u ORCA_CLAUDE_INJECTED_CONFIG_DIR CLAUDE_CONFIG_DIR="$ORCA_CLAUDE_USER_CONFIG_DIR" claude $argv
return $status
end
if test -z "$profile"
env -u CLAUDE_CONFIG_DIR -u ORCA_CLAUDE_INJECTED_CONFIG_DIR claude $argv
return $status
end
if not string match -qr '^(/|[A-Za-z]:)' -- "$profile"
return 1
end
test -d "$profile"; or mkdir -p -m 700 -- "$profile" 2>/dev/null
env CLAUDE_CONFIG_DIR="$profile" ORCA_CLAUDE_INJECTED_CONFIG_DIR="$profile" claude $argv
end
end
set -e __orca_claude_type
`
}
export function getPowerShellClaudeShellFunction(): string {
return `$orcaClaudeCommand = Get-Command claude -ErrorAction SilentlyContinue | Select-Object -First 1
if ($env:ORCA_CLAUDE_PROFILE_POINTER -and $orcaClaudeCommand -and
$orcaClaudeCommand.CommandType -in @("Application", "ExternalScript")) {
function Global:claude {
$names = @('CLAUDE_CONFIG_DIR', 'ORCA_CLAUDE_INJECTED_CONFIG_DIR')
$saved = @{}
foreach ($name in $names) { $saved[$name] = [Environment]::GetEnvironmentVariable($name, 'Process') }
try {
$orcaClaudeHome = ''
if ($env:ORCA_CLAUDE_PROFILE_POINTER -and (Test-Path -LiteralPath $env:ORCA_CLAUDE_PROFILE_POINTER -PathType Leaf)) {
$orcaClaudeHome = [IO.File]::ReadAllText($env:ORCA_CLAUDE_PROFILE_POINTER).TrimEnd()
}
# Why the separator swap: Windows names one folder with either separator.
$own = ("$env:ORCA_CLAUDE_USER_CONFIG_DIR" -replace '/', '\\').TrimEnd('\\')
if ($env:CLAUDE_CONFIG_DIR -and $env:CLAUDE_CONFIG_DIR -ne $env:ORCA_CLAUDE_INJECTED_CONFIG_DIR -and
(-not $own -or ("$env:CLAUDE_CONFIG_DIR" -replace '/', '\\').TrimEnd('\\') -ne $own)) {
# A value set in this pane, other than the user's own, wins.
} elseif (-not $orcaClaudeHome) {
Remove-Item Env:ORCA_CLAUDE_INJECTED_CONFIG_DIR -ErrorAction SilentlyContinue
if ($env:CLAUDE_CONFIG_DIR -and $env:ORCA_CLAUDE_USER_CONFIG_DIR) { $env:CLAUDE_CONFIG_DIR = $env:ORCA_CLAUDE_USER_CONFIG_DIR }
else { Remove-Item Env:CLAUDE_CONFIG_DIR -ErrorAction SilentlyContinue }
} elseif (-not [IO.Path]::IsPathRooted($orcaClaudeHome)) {
$global:LASTEXITCODE = 1
return
} else {
if (-not [IO.Directory]::Exists($orcaClaudeHome)) { $null = New-Item -ItemType Directory -Path $orcaClaudeHome -Force -ErrorAction SilentlyContinue }
$env:CLAUDE_CONFIG_DIR = $orcaClaudeHome
$env:ORCA_CLAUDE_INJECTED_CONFIG_DIR = $orcaClaudeHome
}
$binary = Get-Command claude -CommandType Application,ExternalScript -ErrorAction Stop | Select-Object -First 1
$claudeArgs = @(__OrcaEscapeNativeArgs $binary.Source $args)
if ($MyInvocation.ExpectingInput) { $input | & $binary.Source @claudeArgs } else { & $binary.Source @claudeArgs }
$global:LASTEXITCODE = $LASTEXITCODE
} catch { $global:LASTEXITCODE = 1; Write-Error $_ -ErrorAction Continue }
finally {
# Why Remove-Item: on .NET 9+ a $null value (passed as "") creates the variable empty instead of deleting it.
foreach ($name in $names) {
if ($null -eq $saved[$name]) { Remove-Item -LiteralPath "Env:$name" -ErrorAction SilentlyContinue }
else { [Environment]::SetEnvironmentVariable($name, $saved[$name], 'Process') }
}
}
}
}
Remove-Variable orcaClaudeCommand -ErrorAction SilentlyContinue
`
}