Skip to content

feat(pty-proxy): mirror inner shell cwd via OSC 7#3461

Draft
xav-ie wants to merge 1 commit into
atuinsh:mainfrom
xav-ie:feat/osc7-helper
Draft

feat(pty-proxy): mirror inner shell cwd via OSC 7#3461
xav-ie wants to merge 1 commit into
atuinsh:mainfrom
xav-ie:feat/osc7-helper

Conversation

@xav-ie

@xav-ie xav-ie commented May 1, 2026

Copy link
Copy Markdown
Contributor

Fixes #3296.
Partially addresses #3337 (cwd-tracking portion; shell-integration hooks are a separate problem).

The bug

Terminals and multiplexers (tmux's pane_current_path, Ghostty / Kitty / WezTerm "open in same dir", etc.) read a pane's cwd by inspecting the foreground process via proc_pidinfo (macOS) or /proc/<pid>/cwd (Linux). When atuin pty-proxy is active, the foreground process is the proxy — not the inner shell where the user actually runs cd. The inner shell lives on a private PTY invisible to the multiplexer.

Result: terminal features that depend on knowing the current directory always see the proxy's startup directory, not wherever the user cd-ed to.

The fix

Inner shells emit OSC 7 (\e]7;file://<host><path>\e\\) on every prompt change. The proxy parses the wire payload from the inner-PTY byte stream and chdirs itself to match. Subsequent proc_pidinfo / /proc lookups by the terminal then see the correct cwd.

All path encoding lives in a new atuin pty-proxy emit-osc7 subcommand — each shell's init only needs a one-line hook that calls it. This avoids per-shell URL encoders (4 implementations with subtly different RFC 3986 edge cases) in favor of one well-tested Rust source of truth, while keeping the wire format strictly RFC 3986–compliant for any other terminal listening to OSC 7.

Per-shell hooks:

  • bash / zsh: _atuin_pty_proxy_osc7 runs in precmd (zsh) or PROMPT_COMMAND (bash), gated on $PWD change.
  • fish: --on-variable PWD event handler.
  • nushell: env_change.PWD hook.

Per-prompt cost

The hooks fork+exec atuin pty-proxy emit-osc7 per cd (gated on $PWD change). The atuin shell integration already synchronously forks+execs atuin history start on every preexec, plus a backgrounded atuin history end per command. The OSC 7 emission piggybacks on this existing budget — backgrounded, gated on PWD change — so it adds at most one extra atuin invocation per directory change, strictly less than the existing per-command cost.

Trust boundary

The proxy trusts the OSC 7 wire payload and chdirs based on it. Anyone who can write to the inner PTY can drive the proxy's cwd to any path the user can already chdir to themselves. This is the same trust model as every other OSC sequence terminals honor (OSC 8 hyperlinks, OSC 52 clipboard, etc.) — if attacker-controlled output (e.g. cat /random/file) can spew bytes into your terminal, you've already lost.

Defense-in-depth in the parser:

  • Reject non-absolute paths (OSC 7 paths are absolute by definition; relative = malformed).
  • Reject paths containing .. components (defends against cat /attacker/file redirecting cwd via traversal).
  • 4096-byte cap on parameter buffer (drop, no panic, on overflow).

Alternative considered

Kernel-side cwd lookup (proc_pidinfo / /proc/<inner-shell-pid>/cwd) instead of trusting the wire payload would eliminate the trust boundary entirely — the proxy would query the kernel directly and the wire payload would be a decorative refresh trigger. I explored this on a separate branch and it works, but it adds OS-specific code paths (libproc on macOS, procfs on Linux) and the security delta is small relative to the existing OSC trust model. Happy to revisit if you'd prefer that approach.

Testing

  • 11 unit tests in crates/atuin-pty-proxy/src/osc7.rs, including: streaming chunk boundaries, BEL vs ESC \ terminators, percent-encoded multibyte UTF-8, malformed URI rejection, relative-path rejection, ..-traversal rejection, over-long payload drop.
  • Manually verified end-to-end on macOS with tmux + nushell: cd in the inner shell, then tmux split-window opens a new pane in the right directory.
  • Module gated on #[cfg(unix)] so Windows builds (atuin with default features including pty-proxy) remain unaffected.

Checks

  • I am happy for maintainers to push small adjustments to this PR, to speed up the review cycle
  • I have checked that there are no existing pull requests for the same thing

xav-ie added a commit to xav-ie/atuin that referenced this pull request Jun 16, 2026
Add an OSC 7 (current working directory) path so terminals and
multiplexers that read a pane's cwd via process introspection (e.g. tmux
pane_current_path, Ghostty, Kitty) see the inner shell's directory rather
than the proxy's startup directory.

  - New streaming osc7::Parser (mirrors osc133.rs): parses
    `ESC ] 7 ; file://<host>/<path> ST`, percent-decodes the path, and
    rejects relative paths and `..` traversal (the latter guards against
    attacker-controlled pty output redirecting the proxy's cwd).
  - The pty output loop feeds bytes to the parser and calls
    set_current_dir on each cwd event (silently ignoring non-existent
    paths, e.g. an OSC 7 forwarded from an SSH'd remote).
  - New `atuin pty-proxy emit-osc7` subcommand does the path encoding in
    Rust; each shell's init wires a PWD-change hook (bash PROMPT_COMMAND,
    zsh precmd, fish --on-variable PWD, nu env_change.PWD) that shells out
    to it, gated on ATUIN_PTY_PROXY_SOCKET.

Ported from atuinsh#3461 onto the renamed atuin-pty-proxy crate (was atuin-hex);
the --shell changes that PR was stacked on are intentionally excluded so
this stays focused on OSC 7.
@xav-ie xav-ie force-pushed the feat/osc7-helper branch from 74d57c1 to 590c897 Compare June 16, 2026 16:27
@xav-ie xav-ie changed the title feat(atuin hex): mirror inner shell cwd via OSC 7 feat(pty-proxy): mirror inner shell cwd via OSC 7 Jun 16, 2026
@xav-ie

xav-ie commented Jun 16, 2026

Copy link
Copy Markdown
Contributor Author

Rebased and force-pushed: ported onto the renamed atuin-pty-proxy crate (formerly atuin-hex, renamed in #3473), so this applies cleanly to current main again.

Scoped down to just the OSC 7 feature — this branch was previously stacked on the --shell work (now its own PR, #3327), so those changes are no longer included here.

The OSC 7 streaming parser (osc7.rs), the emit-osc7 subcommand, the per-shell PWD hooks (bash PROMPT_COMMAND / zsh precmd / fish --on-variable PWD / nu env_change.PWD), and the cwd-mirroring in the pty loop all carried over. Relative-path and ..-traversal rejection preserved.

Add an OSC 7 (current working directory) path so terminals and
multiplexers that read a pane's cwd via process introspection (e.g. tmux
pane_current_path, Ghostty, Kitty) see the inner shell's directory rather
than the proxy's startup directory.

  - New streaming osc7::Parser (mirrors osc133.rs): parses
    `ESC ] 7 ; file://<host>/<path> ST`, percent-decodes the path, and
    rejects relative paths and `..` traversal (the latter guards against
    attacker-controlled pty output redirecting the proxy's cwd).
  - The pty output loop feeds bytes to the parser and calls
    set_current_dir on each cwd event (silently ignoring non-existent
    paths, e.g. an OSC 7 forwarded from an SSH'd remote).
  - New `atuin pty-proxy emit-osc7` subcommand does the path encoding in
    Rust; each shell's init wires a PWD-change hook (bash PROMPT_COMMAND,
    zsh precmd, fish --on-variable PWD, nu env_change.PWD) that shells out
    to it, gated on ATUIN_PTY_PROXY_SOCKET.

Ported from atuinsh#3461 onto the renamed atuin-pty-proxy crate (was atuin-hex);
the --shell changes that PR was stacked on are intentionally excluded so
this stays focused on OSC 7.
@xav-ie xav-ie force-pushed the feat/osc7-helper branch from 590c897 to cc09031 Compare June 16, 2026 16:34
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.

[Bug]: When hex is enabled, the working directory is not passed to the Ghostty

1 participant