RCA-2026-04-03-001: bluetoothctl PATH Resolution Failure in Claude Code Shell

Executive Summary

User needed to connect Samsung Galaxy Buds3 Pro urgently before a call. The bluetoothctl binary existed at /usr/bin/bluetoothctl but command -v bluetoothctl returned exit code 127 (command not found). Root cause: the Claude Code shell environment initializes with a malformed $PATH that contains a literal $PATH string instead of the expanded value, preventing resolution of standard /usr/bin/ binaries. Resolution: invoked bluetoothctl via absolute path /usr/bin/bluetoothctl. This RCA documents the shell environment divergence and establishes defensive patterns.

Timeline

Time Event

2026-04-03 ~AM

User requested urgent Bluetooth connection for incoming call

+10s

bluetoothctl devices returned exit code 127 — command not found

+20s

which bluetoothctl also failed (exit 127)

+30s

ls /usr/bin/bluetooth* confirmed binary exists at /usr/bin/bluetoothctl

+40s

/usr/bin/bluetoothctl devices succeeded — listed 8 paired devices

+50s

/usr/bin/bluetoothctl connect 24:24:B7:B5:1C:CC — Buds3 Pro connected

+60s

User on call, requested RCA for learning

Problem Statement

Symptoms

  • bluetoothctl returned exit code 127 (command not found)

  • which bluetoothctl also failed

  • Binary confirmed present at /usr/bin/bluetoothctl

Expected Behavior

Standard binaries in /usr/bin/ should be resolvable by name in any shell session.

Actual Behavior

The Claude Code subshell $PATH contained a literal $PATH string instead of expanded system paths:

$ echo $PATH
/home/evanusmodestus/.local/bin:/home/evanusmodestus/.cargo/bin:/home/evanusmodestus/.local/bin:$PATH

The trailing $PATH was never expanded, so /usr/bin, /usr/sbin, /bin, /sbin were missing from the search path entirely.

Root Cause

5 Whys Analysis

Why # Question and Answer

1

Why wasn’t bluetoothctl found?
Because: /usr/bin was not in the effective $PATH

2

Why wasn’t /usr/bin in $PATH?
Because: The PATH variable contained a literal string $PATH instead of the expanded value

3

Why was $PATH unexpanded?
Because: Somewhere in shell initialization (.zshrc, .zshenv, or .zprofile), a PATH assignment uses double-quoted or unquoted $PATH in a context where it wasn’t expanded — or a config file was written with single quotes around $PATH

4

Why does this only affect Claude Code?
Because: Claude Code spawns a non-interactive or semi-interactive subshell that may not source all profile files, or sources them in a different order than a login terminal

5

Why wasn’t this caught before?
Because: Most commands used in Claude Code sessions are invoked via dedicated tools (Read, Write, Grep, Glob) that don’t depend on $PATH — raw Bash tool use is less common

Root Cause Statement

The Claude Code Bash tool shell environment has a malformed $PATH where the system paths (/usr/bin, /usr/sbin, /bin, /sbin) are missing due to an unexpanded $PATH variable reference in shell initialization. This causes all standard system binaries to be unresolvable by name.

Contributing Factors

Factor Description Preventable?

Shell init order

Claude Code may not source .zshrc / .zshenv in the same order as an interactive terminal

Partially (investigate init files)

PATH construction

A line like export PATH="$HOME/.local/bin:$PATH" may be evaluated in a context where $PATH is empty or literal

Yes (use absolute paths in exports)

Rare Bash tool use

Most work uses dedicated tools; PATH issues go unnoticed

No (by design)

Time pressure

Incoming call compressed troubleshooting time

No

Impact

Severity

Metric Value

Severity

P3 (minor — workaround available)

Duration

~50 seconds to resolution

Users/Systems Affected

1 user, Claude Code shell environment

Data Loss

None

Broader Implications

This affects any Bash tool invocation in Claude Code that relies on standard system binaries by name:

  • systemctl, journalctl, ip, ss, lsblk

  • pacman, paru

  • docker, podman

  • ssh, scp, rsync

Any of these would also fail unless invoked with absolute path.

Resolution

Immediate Action (Workaround)

Used absolute path to invoke the binary:

/usr/bin/bluetoothctl devices
/usr/bin/bluetoothctl connect 24:24:B7:B5:1C:CC

Verification

# Confirmed connection
/usr/bin/bluetoothctl info 24:24:B7:B5:1C:CC | awk '/Name|Connected/'

Shell Environment Deep Dive

How PATH Should Work

1. /etc/zsh/zshenv        ← System-wide, always sourced first
2. ~/.zshenv              ← User, always sourced
3. /etc/zsh/zprofile      ← System-wide, login shells only
4. ~/.zprofile            ← User, login shells only
5. /etc/zsh/zshrc         ← System-wide, interactive shells only
6. ~/.zshrc               ← User, interactive shells only

Standard /usr/bin comes from step 1 (/etc/zsh/zshenv) or is inherited from the parent process.

Debugging Commands

# Show raw PATH
echo $PATH

# Show PATH one-per-line (readable)
echo $PATH | tr ':' '\n'

# Check if /usr/bin is in PATH
echo $PATH | tr ':' '\n' | grep -x '/usr/bin'

# Find where PATH is set in zsh init files
grep -n 'PATH' ~/.zshenv ~/.zshrc ~/.zprofile 2>/dev/null

# Compare Claude Code shell vs terminal
# In terminal:
echo $PATH | tr ':' '\n' | wc -l

# Check what shell Claude Code uses
echo $SHELL
ps -p $$ -o comm=

The Fix Pattern

When constructing PATH in shell init files, ensure system paths are always present:

# DEFENSIVE: Always include system paths explicitly
export PATH="$HOME/.local/bin:$HOME/.cargo/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin"

# OR: Only prepend if PATH already has content
if [[ -n "$PATH" ]]; then
    export PATH="$HOME/.local/bin:$PATH"
else
    export PATH="$HOME/.local/bin:/usr/local/bin:/usr/bin:/bin"
fi

Defensive Patterns for Claude Code

When Bash Tool Fails with Exit 127

# 1. Check if binary exists
ls /usr/bin/<command> /usr/local/bin/<command> 2>/dev/null

# 2. Use absolute path
/usr/bin/<command> <args>

# 3. Or fix PATH inline
PATH="/usr/bin:/usr/sbin:$PATH" <command> <args>

Common Absolute Paths (Arch Linux)

Command Path

bluetoothctl

/usr/bin/bluetoothctl

systemctl

/usr/bin/systemctl

pacman

/usr/bin/pacman

ip

/usr/bin/ip

ss

/usr/bin/ss

docker

/usr/bin/docker

journalctl

/usr/bin/journalctl

Preventive Measures

Short-term (This week)

Action Owner Status

Investigate .zshenv / .zshrc for malformed PATH export

Evan

[ ] Pending

Add defensive PATH construction to shell init

Evan

[ ] Pending

Verify echo $PATH | tr ':' '\n' shows /usr/bin in Claude Code

Evan

[ ] Pending

Long-term

Action Owner Status

Add Claude Code shell environment validation to dotfiles

Evan

[ ] Pending

Document absolute paths for frequently used binaries in codex

Evan

[ ] Pending

Lessons Learned

What went well

  • Quickly identified the binary existed via ls /usr/bin/bluetooth*

  • Absolute path workaround resolved issue in seconds

  • Connected before the call started

What could be improved

  • Should have a pre-validated shell environment for Claude Code

  • PATH issues are silent until they bite — need a health check

Key Takeaways

  1. Exit code 127 = command not found, not command failed — Always check PATH before assuming the tool isn’t installed

  2. Claude Code shell != your terminal — The Bash tool may have a different environment than your interactive zsh session

  3. Absolute paths are the ultimate fallback/usr/bin/command bypasses all PATH issues

  4. ls /usr/bin/command* — Fastest way to verify a binary exists when which also fails (because which itself depends on PATH)

  5. Audit shell init files — A literal $PATH in the output is the smoking gun for unexpanded variable references

Connection to RCA-2026-03-27-001

RCA-2026-03-27-001 addressed the knowledge gap (not knowing bluetoothctl commands). This RCA addresses the environment gap (knowing the command but the shell can’t find it). Together they cover both failure modes for Bluetooth CLI operations.

Metadata

Field Value

RCA ID

RCA-2026-04-03-001

Author

Evan Rosado

Date Created

2026-04-03

Last Updated

2026-04-03

Status

Final

Review Date

2026-05-03