Skip to content

Keep shell shims, wait-for signals and debug logs in private per-user paths - #15179

Closed
austinywang wants to merge 10 commits into
mainfrom
private-shell-state-dirs
Closed

austinywang wants to merge 10 commits into
mainfrom
private-shell-state-dirs

Conversation

@austinywang

@austinywang austinywang commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Some of the files cmux shell integration, the CLI and the app debug logs write sit at fixed names in shared temporary directories. Another local user could create one of those paths first, or point it somewhere else. This PR uses only a private per-user location for these files, and opens each file without following links.

Claude command shim (zsh, bash, fish). _cmux_install_cli_command_shim now puts shims in a directory that must be:

  • a real directory, not a symlink;
  • owned by the current user;
  • not writable by group or others.

Missing directories are created 0700. The parent is checked before the child.

The same check runs before an existing shim root is trusted for lookup. If a root fails the check, the shim is skipped: nothing falls back to a shared path. An inherited root that fails the check is also taken out of PATH.

Remote hosts get the same fix, because RemoteInteractiveShellBootstrapBuilder embeds these integration files.

Bash hint and history state. These files used to go straight into ${TMPDIR:-/tmp}. They now go in …/cmux-bash-$EUID, which is held to the same private-directory rule.

cmux wait-for. Signal files move out of /tmp/cmux-wait-for-<name>.sig into cmux-wait-for under the per-user temporary directory (confstr(_CS_DARWIN_USER_TEMP_DIR)). That directory doesn't depend on TMPDIR, so the signalling side and the waiting side resolve the same path.

  • The directory must be a real 0700-style directory owned by this user, or wait-for fails with an error.
  • -S creates the signal file with openat(O_NOFOLLOW), relative to the verified directory.
  • The waiter accepts only a regular file owned by this user, checked with fstatat(AT_SYMLINK_NOFOLLOW), and then removes it.
  • The waiter now uses a kqueue watch on the directory, with a recheck at least once per second, replacing the old path poller.
  • A signal sent before the wait still wakes the waiter.

App debug logs. Three logs share a new helper, OwnedLogFile.openForAppending:

  • /tmp/cmux-bg.log
  • /tmp/cmux-ghostty-init.log
  • /tmp/cmux-panel-debug.log

The helper opens with O_NOFOLLOW and keeps the file only if it's a regular file owned by this user with a single link. New log files are created 0600.

Out of scope, as follow-ups:

  • The app-side Swift shim writer (TerminalSurface+AgentCommandShims.swift) should get the same owner and symlink checks.
  • The ROOT PATH prepend in WorkspaceInitialCommandLoginShell should get the same checks.
  • Nushell integration isn't covered.

#14642 edits the same three shell-integration files, so whichever lands second will need a rebase.

Testing

Each fix follows its failing test in the commit history. For each pair below, the red and green results come from the same command.

Area Command Red Green
Shell shim and bash state (zsh, bash, fish) python3 tests/test_shell_cli_shim_private_dir.py 54952742ca: exit 1, 10 tests, FAILED (failures=38) 51add4c847: exit 0, 10 tests, OK
cmux wait-for python3 tests/test_cli_tmux_wait_for_private_dir.py ac1c6004ec: exit 1, 6 tests, FAILED (failures=3) 9e5b2c58ff: exit 0, 6 tests, OK
Debug logs swift test --package-path Packages/macOS/CmuxFoundation --filter OwnedLogFileTests 7556730023: exit 1, 5 tests, 4 failed with 6 issues af076601a4: exit 0, 5 passed

Notes on the red commits:

  • The wait-for red commit moves the existing signal code into CLI/TmuxWaitForSignal.swift unchanged, so the test can compile it on its own.
  • The debug-log red commit moves the three writers onto OwnedLogFile with their old behavior.

At head d780d95035:

  • Both Python tests pass again.
  • python3 scripts/verify-local.py --affected origin/main --swift-changed origin/main passed 15/15 checks.

These related shell tests also pass on this branch:

  • test_claude_wrapper_shim_root_survives_tmpdir_change
  • test_issue_9356_bash_shim_noclobber
  • test_issue_6714_zsh_shim_noclobber
  • test_claude_wrapper_mutual_shim_loop
  • test_issue_2448_shell_claude_wrapper_dispatch
  • test_issue_13343_claude_integration_toggle
  • test_claude_wrapper_user_binary_resolution
  • test_shell_first_prompt_spawns
  • test_bash_integration_no_done_notifications
  • test_shell_no_git_watch

test_nushell_shim_path_refront was skipped because nu isn't installed.

Not verified:

  • No app or CLI build. Neither was built, even in a tagged build, and cmuxTests didn't run. The app log call sites and the FileBackgroundLogLineSink change haven't been compiled. The wait-for code was compiled only on its own, with swiftc -swift-version 5.
  • Remote hosts weren't exercised.
  • The check that rejects files owned by another user wasn't tested, since that needs a second account. The symlink, hard-link, permission and missing-path cases are tested.
  • The wait-for test changes the real per-user signal directory. It briefly sets cmux-wait-for to 0770 and restores it in finally.

Known limits:

  • If TMPDIR is world-writable and not sticky, another user could still rename the checked directory between the check and its use.
  • On a shared host, if another user already owns /tmp/cmux-cli-shims, the shim is disabled instead of falling back.

Localization: no user-facing UI strings changed. The new wait-for errors are non-localized CLIError messages, like the existing wait-for errors.

Changelog

Fixed: The Claude command shim, bash history state, cmux wait-for signals and debug logs no longer use shared temporary paths that another local user could create or redirect.

Checklist

  • Behavior changes have added or updated tests, or Testing says why not
  • UI, settings, menu, schema, help-text or user-facing docs change: localization audited, and the result is stated above
  • Reviewed with a subagent before merge (cmux-review), and all bot and human review comments resolved

🤖 Generated with Claude Code


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.


Summary by cubic

Moves shell command shims, cmux wait-for signal files, bash history state, and debug logs out of shared temporary paths into private per-user locations, so another local user can no longer pre-create or redirect those paths.

Behavior

  • Shell shims (zsh, bash, fish) now run only from a real directory owned by the current user with no group or other write bit; a missing directory is created 0700, and an inherited root that fails the check is replaced or removed from PATH. If no private shim root can be made, the shim is skipped and the bundled wrapper runs instead.
  • The bash hint and history state files move under <TMPDIR>/cmux-bash-<euid>, held to the same private-directory rule.
  • cmux wait-for signal files move to the per-user temp directory, resolved via confstr so both ends agree regardless of TMPDIR; files are created and consumed without following symlinks, and the waiter uses a kqueue on the verified directory instead of polling the file path.
  • The /tmp debug logs (cmux-bg.log, cmux-ghostty-init.log, cmux-panel-debug.log) open with O_NOFOLLOW and are kept only when owned by the current user with a single link; new files are 0600.

Notes

  • Nushell integration, the app-side Swift shim writer, and the ROOT PATH prepend are follow-ups.
  • The workspace shim root owner test from main passes on this branch.
  • If TMPDIR is world-writable and not sticky, a check-to-use race remains; on a shared host an already-owned shim directory disables the shim rather than replacing it.
  • The app and CLI weren't built in this PR; only the shell integrations, the wait-for signal owner, and the OwnedLogFile helper are covered by tests.

Written for commit 9d58d1d. Summary will update on new commits.

Review in cubic

austinywang and others added 7 commits September 27, 2026 22:08
…ectories

The claude shim directory and bash's PR-hint and history files are created
under TMPDIR with mkdir -p and shell redirection, which accept a directory
another user created and follow symlinks. These cases pre-create shared,
group-writable and symlinked directories and check that zsh, bash and fish
leave them alone, keep them off PATH and never run what they contain.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ories

The zsh, bash and fish integrations now write and trust the claude CLI shim
only from a per-user directory that is ours, not a symlink, and not group- or
other-writable. A missing directory is created 0700. An inherited shim root
that fails the check falls back to a fresh private root, and if none can be
made the shim is skipped and the bundled wrapper runs instead. The claude
function only runs the shim this shell verified.

The bash PR-hint and history scratch files move into the same kind of
private directory, and the tracked background job no longer needs a pid file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Moves the wait-for signal file handling out of cmux.swift, unchanged, into
TmuxWaitForSignal so a small driver can compile and exercise it. The new test
fails today: signals land in the shared /tmp, and a waiter accepts a symlink
at the signal path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signal files move from the shared /tmp into cmux-wait-for under the per-user
temporary directory, which comes from confstr so both ends agree regardless
of TMPDIR. The directory is created 0700 and must be a real directory we own
with no group or other write bit. Signals are created with O_NOFOLLOW relative
to that directory, and a waiter only accepts a regular file we own. The wait
uses a kqueue on the verified directory instead of the path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Moves the three /tmp debug log writers onto one OwnedLogFile helper with
their current behavior, and adds tests that a log path which is a
symlink, a hard link or a missing file is handled privately. They fail
until the helper is hardened.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The /tmp debug logs (cmux-bg.log, cmux-ghostty-init.log and
cmux-panel-debug.log) now open with O_NOFOLLOW and are kept only when
the result is a regular file this user owns with a single link. New log
files are created 0600.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Sep 28, 2026 •

Copy link
Copy Markdown

Warning

Review limit reached

Next included review available in 1 minute.

Check out review usage here.

View limit details

Limit details: You’ve used all 10 included reviews currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Repository: manaflow-ai/cmux/.coderabbit.yaml

Review profile: ASSERTIVE

Plan: Advanced

Run ID: e6653ba5-850c-4556-9cef-a89b1964aece

📥 Commits

Reviewing files that changed from the base of the PR and between ce5cb45 and 9d58d1d.

📒 Files selected for processing (15)
  • CLI/TmuxWaitForSignal.swift
  • CLI/cmux.swift
  • Packages/macOS/CmuxFoundation/Sources/CmuxFoundation/OwnedLogFile.swift
  • Packages/macOS/CmuxFoundation/Tests/CmuxFoundationTests/OwnedLogFileTests.swift
  • Packages/macOS/CmuxTerminalCore/Sources/CmuxTerminalCore/DebugSupport/FileBackgroundLogLineSink.swift
  • Resources/shell-integration/cmux-bash-integration.bash
  • Resources/shell-integration/cmux-zsh-integration.zsh
  • Resources/shell-integration/fish/config.fish
  • Sources/GhosttyTerminalView.swift
  • Sources/WorkspaceContentView.swift
  • cmux.xcodeproj/project.pbxproj
  • tests/fixtures/TmuxWaitForSignalFixture.swift
  • tests/test-execution.toml
  • tests/test_cli_tmux_wait_for_private_dir.py
  • tests/test_shell_cli_shim_private_dir.py

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

Copy link
Copy Markdown
Contributor

All contributors have signed the CLA ✍️ ✅
Posted by the CLA Assistant Lite bot.

@blacksmith-sh

This comment has been minimized.

@github-actions

github-actions Bot commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

CI failure attribution

CI passes on 9d58d1d493 (run 36486183818 attempt 4).

Written by scripts/ci/classify_failures.py (ci-failure-attribution.yml); signatures are its SIGNATURES table. A machine verdict is the runner's fault, not this PR's.

@github-actions

Copy link
Copy Markdown
Contributor

Automatic catch-up couldn't merge main (0fc4975dba5c): tests/test-execution.toml (both sides changed it). Nothing was pushed; merge it by hand. A new push or /catch-up tries again.

Label no-auto-catch-up to opt out · Catch-up run

tests/test-execution.toml: keep this branch's shell shim test and
main's workspace shim root owner test (#15185) as separate macos-shell
entries.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@cursor

cursor Bot commented Sep 28, 2026

Copy link
Copy Markdown

Bugbot is paused — on-demand spend limit reached

Bugbot uses usage-based billing for this team and has hit its on-demand spend limit.

A team admin can raise the spend limit in the Cursor dashboard, or wait for the next billing cycle to continue.

austinywang and others added 2 commits September 28, 2026 14:26
The package-conventions lint rejects all-static namespace types.
OwnedLogFile is now a struct holding the log path and the user that
must own it, like PrivateDirectoryCheck, and callers open it with
OwnedLogFile(path:).openForAppending(). The injected owner also lets
the tests cover a file another user owns.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The wake-on-later-signal test slept half a second and then asserted on
elapsed time, which the test-determinism guard rejects. wait(timeout:)
now takes a callback that runs once the directory watch is registered
and the first check found no signal. The fixture reports it on stderr,
the test signals only after reading it, and the waiter's own timeout is
far past communicate()'s, so its OK can only come from the signal.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

Dogfood tours of 9d58d1d4

modifier-clicks-tour at 9d58d1d4, on its merge 5f08e4be that CI built: passed (run)

modifier-clicks-tour at 9d58d1d4

Key frames of modifier-clicks-tour at 9d58d1d 08-01-plain-hover-example 11-01-cmd-hover-example 14-01-cmd-click-opens-example 21-02-cmd-click-opens-github

sidebar-and-chrome-tour at 9d58d1d4, on its merge 5f08e4be that CI built: passed (run)

sidebar-and-chrome-tour at 9d58d1d4

Key frames of sidebar-and-chrome-tour at 9d58d1d 04-three-workspaces 10-split-right 15-command-palette 24-settings

Tours are picked by the paths globs in dogfood/scenarios/*.json; a Dogfood-tours: a, b line in the description picks them instead (none turns this off). Look at every frame before merging: a green tour only means no step failed.

austinywang added a commit that referenced this pull request Sep 28, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
github-actions Bot added a commit that referenced this pull request Sep 28, 2026
github-actions Bot added a commit that referenced this pull request Sep 28, 2026
github-actions Bot added a commit that referenced this pull request Sep 28, 2026
github-actions Bot added a commit that referenced this pull request Sep 28, 2026
github-actions Bot added a commit that referenced this pull request Sep 28, 2026
github-actions Bot added a commit that referenced this pull request Sep 28, 2026
github-actions Bot added a commit that referenced this pull request Sep 28, 2026
github-actions Bot added a commit that referenced this pull request Sep 28, 2026
github-actions Bot added a commit that referenced this pull request Sep 28, 2026
github-actions Bot added a commit that referenced this pull request Sep 28, 2026
github-actions Bot added a commit that referenced this pull request Sep 28, 2026
github-actions Bot added a commit that referenced this pull request Sep 28, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Automatic catch-up couldn't merge main (ad7906a2611f): CLI/TmuxWaitForSignal.swift (added on both sides; needs a person), Packages/macOS/CmuxFoundation/Sources/CmuxFoundation/OwnedLogFile.swift (added on both sides; needs a person), Packages/macOS/CmuxFoundation/Tests/CmuxFoundationTests/OwnedLogFileTests.swift (added on both sides; needs a person), Resources/shell-integration/cmux-bash-integration.bash (both sides changed it), Resources/shell-integration/cmux-zsh-integration.zsh (both sides changed it), 3 more in the run log. Nothing was pushed; merge it by hand. A new push or /catch-up tries again.

Label no-auto-catch-up to opt out · Catch-up run

@austinywang

Copy link
Copy Markdown
Contributor Author

Closing as superseded by merged PR #15116, which consolidated this SSH/security/local-state hardening into main.

@austinywang austinywang closed this Oct 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant