Require an absolute Orca CLI for the agent-teams tmux shim (#14438)

The generated tmux shim fell back to a bare orca / orca.cmd / orca-ide, and cmd.exe resolves an unqualified command against the current directory before PATH (sh does the same via ./empty PATH entries), so a stray orca.cmd in an agent's checkout could run with the agent-teams team id and token in its environment.

Resolve only absolute paths, honor the Windows Path env spelling, degrade to in-process teammates when no CLI can be qualified, and make both shims exit 127 instead of guessing. Verified on macOS, Linux (dash + bash), and Windows (cmd.exe + Git Bash).

Fixes STA-4215.
This commit is contained in:
Neil
2026-08-13 22:23:52 -07:00
committed by GitHub
parent bac21ee64d
commit b04da03ffc
4 changed files with 166 additions and 20 deletions
@@ -26,7 +26,8 @@ export class ClaudeAgentTeamsService {
leaderHandle: string
baseEnv: Record<string, string | undefined>
shimDir: string
shimBin: string
/** Absolute path only; null leaves the var unset so the shim refuses to guess a cwd-relative CLI. */
shimBin: string | null
}): AgentTeamsLaunchEnv {
const teamId = `team-${randomUUID()}`
const token = randomBytes(32).toString('base64url')
@@ -47,8 +48,10 @@ export class ClaudeAgentTeamsService {
ORCA_AGENT_TEAMS_TEAM_ID: teamId,
ORCA_AGENT_TEAMS_TOKEN: token,
ORCA_AGENT_TEAMS_LEADER_PANE: leaderPane,
ORCA_AGENT_TEAMS_SHIM_DIR: args.shimDir,
ORCA_AGENT_TEAMS_SHIM_BIN: args.shimBin
ORCA_AGENT_TEAMS_SHIM_DIR: args.shimDir
}
if (args.shimBin) {
env.ORCA_AGENT_TEAMS_SHIM_BIN = args.shimBin
}
if (args.baseEnv.ORCA_PAIRING_CODE) {
env.ORCA_PAIRING_CODE = args.baseEnv.ORCA_PAIRING_CODE
@@ -1,3 +1,5 @@
import { spawnSync } from 'node:child_process'
import { existsSync } from 'node:fs'
import { chmod, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
@@ -5,7 +7,8 @@ import { afterEach, describe, expect, it } from 'vitest'
import {
buildClaudeAgentTeamsLaunchPlan,
ensureClaudeAgentTeamsShimDir,
resolveClaudeAgentTeamsShimBin
resolveClaudeAgentTeamsShimBin,
windowsClaudeAgentTeamsShimScript
} from './claude-agent-teams-shim-env'
const roots: string[] = []
@@ -88,4 +91,105 @@ describe('claude agent teams shim env', () => {
expect(resolveClaudeAgentTeamsShimBin({ PATH: root })).toBe(cliPath)
})
it('refuses to resolve a CLI through relative PATH entries or a bare override', async () => {
const root = await mkdtemp(join(tmpdir(), 'orca-agent-teams-cli-'))
roots.push(root)
for (const name of ['orca', 'orca-ide', 'orca.cmd']) {
const path = join(root, name)
await writeFile(path, '#!/usr/bin/env sh\n', 'utf8')
if (process.platform !== 'win32') {
await chmod(path, 0o755)
}
}
expect(resolveClaudeAgentTeamsShimBin({ PATH: '.' })).toBeNull()
expect(resolveClaudeAgentTeamsShimBin({ PATH: '' })).toBeNull()
expect(
resolveClaudeAgentTeamsShimBin({ PATH: '.', ORCA_AGENT_TEAMS_SHIM_BIN: 'orca' })
).toBeNull()
// Why: a bare override is still honored when it maps to a real absolute PATH entry.
expect(resolveClaudeAgentTeamsShimBin({ PATH: root, ORCA_AGENT_TEAMS_SHIM_BIN: 'orca' })).toBe(
join(root, 'orca')
)
})
it.skipIf(process.platform !== 'win32')(
'resolves through the Windows `Path` env spelling',
async () => {
const root = await mkdtemp(join(tmpdir(), 'orca-agent-teams-cli-'))
roots.push(root)
const cliPath = join(root, 'orca.cmd')
await writeFile(cliPath, '@echo off\r\n', 'utf8')
expect(resolveClaudeAgentTeamsShimBin({ Path: root })).toBe(cliPath)
}
)
it('falls back to in-process teammates when no absolute CLI can be qualified', async () => {
const createTeamEnv = (): Record<string, string> => {
throw new Error('native shim env must not be built without a qualified CLI')
}
await expect(
buildClaudeAgentTeamsLaunchPlan({
command: 'claude',
mode: 'native-panes-shim',
baseEnv: { PATH: '.' },
createTeamEnv
})
).resolves.toEqual({
command: 'claude --teammate-mode in-process',
env: { CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS: '1' }
})
})
it.skipIf(process.platform === 'win32')(
'never runs a cwd-resolved orca when the shim bin is unqualified',
async () => {
const root = await mkdtemp(join(tmpdir(), 'orca-agent-teams-shim-'))
roots.push(root)
await ensureClaudeAgentTeamsShimDir(root)
const cwd = await mkdtemp(join(tmpdir(), 'orca-agent-teams-cwd-'))
roots.push(cwd)
const marker = join(cwd, 'hijacked')
for (const name of ['orca', 'orca-ide']) {
const decoy = join(cwd, name)
await writeFile(decoy, `#!/usr/bin/env sh\ntouch ${JSON.stringify(marker)}\n`, 'utf8')
await chmod(decoy, 0o755)
}
const hijack = spawnSync(join(root, 'tmux'), ['display-message', '-p', '#{pane_id}'], {
cwd,
env: { PATH: `.:${process.env.PATH ?? ''}` },
encoding: 'utf8'
})
expect(hijack.status).toBe(127)
expect(hijack.stderr).toContain('absolute path')
expect(existsSync(marker)).toBe(false)
const cli = join(cwd, 'fake-orca')
await writeFile(cli, '#!/usr/bin/env sh\necho "ran $*"\n', 'utf8')
await chmod(cli, 0o755)
const qualified = spawnSync(join(root, 'tmux'), ['list-panes'], {
cwd,
env: { PATH: `.:${process.env.PATH ?? ''}`, ORCA_AGENT_TEAMS_SHIM_BIN: cli },
encoding: 'utf8'
})
expect(qualified.status).toBe(0)
expect(qualified.stdout.trim()).toBe('ran agent-teams-tmux list-panes')
}
)
it('writes a Windows shim that rejects an unqualified shim bin', () => {
const script = windowsClaudeAgentTeamsShimScript()
expect(script).not.toMatch(/^set "ORCA_AGENT_TEAMS_SHIM_BIN=orca/m)
expect(script).toContain('if "%ORCA_SHIM_BIN:~1,1%"==":" goto :run')
// Why: `call` would re-expand `%2`-style tmux pane args as batch parameters.
expect(script).toContain('\r\n"%ORCA_SHIM_BIN%" agent-teams-tmux %*\r\n')
expect(script).toContain('exit /b 127')
})
})
+51 -16
View File
@@ -1,7 +1,7 @@
import { chmod, mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises'
import { accessSync, constants, existsSync } from 'node:fs'
import { homedir } from 'node:os'
import { delimiter, dirname, join } from 'node:path'
import { delimiter, dirname, isAbsolute, join } from 'node:path'
import {
addClaudeTeammateModeAuto,
addClaudeTeammateModeInProcess,
@@ -9,6 +9,7 @@ import {
type ClaudeAgentTeamsMode
} from '../../shared/claude-agent-teams-tmux-compat'
import { getOrcaCliCommandNameForPlatform } from '../../shared/orca-cli-command-name'
import { resolvePathEnvKey } from '../pty/windows-path-segment-merge'
export type ClaudeAgentTeamsLaunchPlan = {
command: string
@@ -20,7 +21,7 @@ export async function ensureClaudeAgentTeamsShimDir(root = defaultShimRoot()): P
await mkdir(root, { recursive: true })
await writeIfChanged(join(root, 'tmux'), unixShimScript())
if (process.platform === 'win32') {
await writeIfChanged(join(root, 'tmux.cmd'), windowsShimScript())
await writeIfChanged(join(root, 'tmux.cmd'), windowsClaudeAgentTeamsShimScript())
}
return root
}
@@ -41,8 +42,15 @@ export async function buildClaudeAgentTeamsLaunchPlan(args: {
env: { CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS: '1' }
}
}
const shimDir = await ensureClaudeAgentTeamsShimDir()
const shimBin = resolveClaudeAgentTeamsShimBin(args.baseEnv)
if (!shimBin) {
// Why: without an absolute CLI path the shim would resolve a bare `orca` against the pane cwd, so degrade instead.
return {
command: addClaudeTeammateModeInProcess(args.command),
env: { CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS: '1' }
}
}
const shimDir = await ensureClaudeAgentTeamsShimDir()
const env = args.createTeamEnv(shimDir, shimBin)
return {
command: addClaudeTeammateModeAuto(args.command),
@@ -51,20 +59,27 @@ export async function buildClaudeAgentTeamsLaunchPlan(args: {
}
}
/** Absolute path to the Orca CLI that backs the tmux shim, or null when none can be qualified. */
export function resolveClaudeAgentTeamsShimBin(
env: Record<string, string | undefined> = process.env
): string {
if (env.ORCA_AGENT_TEAMS_SHIM_BIN) {
return env.ORCA_AGENT_TEAMS_SHIM_BIN
): string | null {
// Why: Windows callers pass an env spelt `Path`; reading only `PATH` there would find no CLI at all.
const pathValue = env[resolvePathEnvKey(env, process.platform)]
const override = env.ORCA_AGENT_TEAMS_SHIM_BIN
if (override) {
// Why: a bare override name would be resolved by the shim's shell against its cwd, so qualify it or ignore it.
const qualified = isAbsolute(override) ? override : findExecutableOnPath(override, pathValue)
if (qualified) {
return qualified
}
}
const bundled = bundledLauncherPath()
if (bundled && isExecutableFile(bundled)) {
return bundled
}
return (
findExecutableOnPath(process.platform === 'win32' ? 'orca-dev.cmd' : 'orca-dev', env.PATH) ??
findExecutableOnPath(getOrcaCliCommandNameForPlatform(process.platform), env.PATH) ??
getOrcaCliCommandNameForPlatform(process.platform)
findExecutableOnPath(process.platform === 'win32' ? 'orca-dev.cmd' : 'orca-dev', pathValue) ??
findExecutableOnPath(getOrcaCliCommandNameForPlatform(process.platform), pathValue)
)
}
@@ -90,7 +105,8 @@ function bundledLauncherPath(): string | null {
function findExecutableOnPath(command: string, pathValue: string | undefined): string | null {
for (const directory of pathValue?.split(delimiter) ?? []) {
if (!directory) {
// Why: empty and relative PATH entries resolve against a cwd we do not control, which is the hijack we are avoiding.
if (!directory || !isAbsolute(directory)) {
continue
}
const candidate = join(directory, command)
@@ -113,23 +129,42 @@ function isExecutableFile(candidate: string): boolean {
}
}
// Why: an unqualified command name is resolved against the invoking pane's cwd (always on cmd.exe, and via `.`/empty
// PATH entries on POSIX), so a stray `orca` next to the agent's files would run with the team token. Demand a
// fully-qualified binary instead of guessing one.
function unixShimScript(): string {
return [
'#!/usr/bin/env sh',
'set -eu',
`exec "\${ORCA_AGENT_TEAMS_SHIM_BIN:-${getOrcaCliCommandNameForPlatform(process.platform)}}" agent-teams-tmux "$@"`,
'orca_bin=${ORCA_AGENT_TEAMS_SHIM_BIN:-}',
'case $orca_bin in',
' /*|[A-Za-z]:[\\\\/]*) ;;',
' *)',
' echo "orca agent-teams tmux shim: ORCA_AGENT_TEAMS_SHIM_BIN must be an absolute path" >&2',
' exit 127',
' ;;',
'esac',
'exec "$orca_bin" agent-teams-tmux "$@"',
''
].join('\n')
}
function windowsShimScript(): string {
export function windowsClaudeAgentTeamsShimScript(): string {
return [
'@echo off',
'setlocal',
'if "%ORCA_AGENT_TEAMS_SHIM_BIN%"=="" (',
` set "ORCA_AGENT_TEAMS_SHIM_BIN=${getOrcaCliCommandNameForPlatform(process.platform)}"`,
')',
'"%ORCA_AGENT_TEAMS_SHIM_BIN%" agent-teams-tmux %*',
'set "ORCA_SHIM_BIN=%ORCA_AGENT_TEAMS_SHIM_BIN%"',
'if not defined ORCA_SHIM_BIN goto :unqualified',
'if "%ORCA_SHIM_BIN:~1,1%"==":" goto :run',
'if "%ORCA_SHIM_BIN:~0,2%"=="\\\\" goto :run',
'goto :unqualified',
':run',
// Why: no `call` — its extra percent-expansion pass would rewrite tmux pane args such as `%2` into batch parameters.
'"%ORCA_SHIM_BIN%" agent-teams-tmux %*',
'exit /b %ERRORLEVEL%',
':unqualified',
'echo orca agent-teams tmux shim: ORCA_AGENT_TEAMS_SHIM_BIN must be an absolute path 1>&2',
'exit /b 127',
''
].join('\r\n')
}
+4
View File
@@ -14263,6 +14263,8 @@ describe('OrcaRuntimeService', () => {
command: 'claude --resume claude-session',
env: {
CLAUDE_PROFILE: 'captured',
// Why: native panes need an absolute CLI; without one the plan degrades to in-process teammates.
ORCA_AGENT_TEAMS_SHIM_BIN: '/opt/orca/bin/orca-ide',
ORCA_AGENT_TEAMS_TEAM_ID: 'stale-team',
ORCA_AGENT_TEAMS_TOKEN: 'stale-token',
TMUX: '/tmp/orca-claude-agent-teams/stale-team,0,1'
@@ -14273,6 +14275,7 @@ describe('OrcaRuntimeService', () => {
agentArgs: '--teammate-mode auto',
agentEnv: {
CLAUDE_PROFILE: 'captured',
ORCA_AGENT_TEAMS_SHIM_BIN: '/opt/orca/bin/orca-ide',
ORCA_AGENT_TEAMS_TEAM_ID: 'stale-team',
ORCA_AGENT_TEAMS_TOKEN: 'stale-token',
TMUX: '/tmp/orca-claude-agent-teams/stale-team,0,1'
@@ -14287,6 +14290,7 @@ describe('OrcaRuntimeService', () => {
expect(spawnCall?.env).toMatchObject({
CLAUDE_PROFILE: 'captured',
CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS: '1',
ORCA_AGENT_TEAMS_SHIM_BIN: '/opt/orca/bin/orca-ide',
TMUX_PANE: '%1'
})
expect(spawnCall?.env?.ORCA_AGENT_TEAMS_TEAM_ID).toMatch(/^team-/)