Skip to content

fix(compat): take the basename of the whole ps comm path - #771

Open
cattyneo wants to merge 1 commit into
fujibee:mainfrom
cattyneo:fix/macos-comm-path-with-spaces
Open

fix(compat): take the basename of the whole ps comm path#771
cattyneo wants to merge 1 commit into
fujibee:mainfrom
cattyneo:fix/macos-comm-path-with-spaces

Conversation

@cattyneo

Copy link
Copy Markdown

Fixes #770.

What

compat_get_comm's POSIX branch piped ps -o comm= through xargs basename. On macOS ps -o comm= prints the full executable path, and xargs splits it on whitespace — so basename got the part before the first space as NAME and the remainder as SUFFIX:

basename "/Users/u/Library/Application" "Support/Claude/.../MacOS/claude"  ->  Application

The Claude Code desktop app always installs its CLI under ~/Library/Application Support/Claude/claude-code/<ver>/claude.app/Contents/MacOS/claude, so every desktop-app session resolved its agent to Application. agmsg_pid_is_agent never matched, agmsg_agent_pid walked straight past the agent to launchd and returned 1, the instance id degraded to a bare session id, cc-instance.<pid> was never written, and actas_lock_gc_stale then removed the lock on the next session-start.sh from any other session — with the watcher still alive and still delivering, so nothing surfaced.

Full analysis, the consequence chain with line numbers, and a same-machine control (space-free installs on the same host resolve correctly) are in #770.

How

Take the basename of the whole string:

--- a/scripts/lib/compat.sh
+++ b/scripts/lib/compat.sh
@@ -110,7 +110,14 @@ compat_get_comm() {
       fi
       ;;
     *)
-      ps -o comm= -p "$pid" 2>/dev/null | xargs basename 2>/dev/null
+      # `ps -o comm=` prints the executable path on macOS. Piping it through
+      # `xargs basename` splits that path on whitespace (and eats quotes), so a
+      # binary under e.g. "~/Library/Application Support/..." resolves to
+      # "Application". Take the basename of the whole string instead.
+      local _comm
+      _comm=$(ps -o comm= -p "$pid" 2>/dev/null) || return 1
+      [ -n "$_comm" ] || return 1
+      basename -- "$_comm" 2>/dev/null
       ;;
   esac
 }

Behaviour changes only where the old form was already wrong:

input before after
/usr/local/bin/claude claude claude (unchanged)
/Users/u/Library/Application Support/.../claude Application claude
/opt/it's here/claude (xargs error, empty) claude
unknown pid `` + exit 0 `` + non-zero

-- guards a path starting with -.

Scope

POSIX branch only. compat.sh:96 (msys, /proc/<pid>/cmdline) has the same xargs basename shape and C:\Program Files\... would hit it, but I have no Windows environment to test on, so I left it alone rather than ship an untested change. Happy to extend the PR if you want it. (:109 reads awk '{print $NF}', which is already whitespace-split, so it is unaffected.)

agmsg_pid_is_agent's awk '{print $1}' on ps -o args= has the same weakness, but with comm fixed it is a fallback that is not normally reached — also left alone.

Tests

tests/test_compat_posix.bats, 5 tests, skipped on MSYS (the msys branch stays covered by test_compat.bats).

Four stubbed-ps tests are deterministic on any POSIX host: space in the path, quote in the path, no-space regression guard, and unknown pid. A fifth test does not use the stub — it symlinks the real sleep under a directory with a space, runs it, and checks the real ps against compat_get_comm, so the stub's premise is verified against the platform rather than assumed. Where ps reports a bare comm instead of a path (Linux) that test skips with a message, because the splitting path is unreachable there.

(A copy of /bin/sleep rather than a symlink does not work for that test on macOS: the copy is SIGKILLed for an invalid code signature and ps then reports nothing, which made the test skip for the wrong reason.)

Break-test contrast on macOS 15.5:

# before (compat.sh reverted, tests present)
not ok 1 ... path containing spaces
not ok 2 ... path containing a quote
ok     3 ... unchanged for a path without spaces      <- regression guard, passes both ways
not ok 4 ... fails for a pid ps knows nothing about
not ok 5 ... matches the real ps for a binary under a space-containing path

# after
ok 1 / ok 2 / ok 3 / ok 4 / ok 5

bats tests/ full suite on macOS 15.5: 912 tests, 0 failures, 12 skipped (exit 0).

`ps -o comm=` prints the full executable path on macOS. Piping it through
`xargs basename` word-splits that path, so `basename` receives the part
before the first space as NAME and the rest as SUFFIX.

The Claude Code desktop app always installs its CLI under
`~/Library/Application Support/Claude/claude-code/<ver>/...`, so
`compat_get_comm` returned `Application` there. `agmsg_pid_is_agent` then
never matched, `agmsg_agent_pid` walked past the agent up to launchd and
failed, the instance id degraded to a bare session id, `cc-instance.<pid>`
was never written, and every actas lock was GC'd as stale on the next
`session-start.sh` — silently, while the watcher stayed alive and kept
delivering.

Take the basename of the whole string instead. Paths without spaces behave
as before; paths that `xargs` would have choked on (quotes) now work too,
and an unknown pid returns non-zero rather than an empty string.

Scope: the POSIX branch only. The msys branch has the same `xargs basename`
shape at compat.sh:96, which `C:\Program Files\...` would hit, but that is
untested here.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@cattyneo
cattyneo force-pushed the fix/macos-comm-path-with-spaces branch from 444cd71 to 3118ea5 Compare August 13, 2026 13:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant