Skip to content

Integrate with common shells in order to track cwd - #41

Open
savetheclocktower wants to merge 3 commits into
hyperlinksfrom
shell-integration-for-cwd
Open

Integrate with common shells in order to track cwd#41
savetheclocktower wants to merge 3 commits into
hyperlinksfrom
shell-integration-for-cwd

Conversation

@savetheclocktower

@savetheclocktower savetheclocktower commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

…and support some other future features.

FULL DISCLOSURE: I knew this was the anti-sweet spot of

  • important feature with lots of potential pitfalls
  • touching many areas of uncertainty for me (four different shell script syntaxes!)
  • already having been implemented robustly by a different editor with a compatible license
  • delivering functionality which I recognize as worth providing yet which I find completely boring.

Also: in some senses VS Code's source is rather straightforward to read; in others, it's utterly infuriating. If you're consulting their implementation of something as you implement a feature, you'll go off on labyrinthine tangents as you chase types and utility functions through a web of interlocked files.

So this was the perfect feature to hand to Claude Code with some very explicit guidance. I had copied over the shell integration files and done some imperfect variable renaming. I had also started to adapt the method that performed injection for each type of shell and had gotten tired of all the tendrils, so that's where I handed it off to Claude.

VS Code came up with their own terminal escape sequences so that a host could send metadata to a terminal about the session. One of the pieces of metadata that can be sent over OSC 633 is the current working directory. That's the main one we care about for this feature, so that's all that's been implemented.

Deep-dive on OSC 633 and terminals

The architecture is pretty clever! OSC 633 is about communicating metadata about a terminal session in an in-band way. There's no extra band for metadata, so it's got to hide itself in the signal. This is very similar to how phones worked in the analog days; instead of having a separate channel for controlling the phone, all control had to happen over the same band as the actual voice communication. Hence first rotary phones (numbers of clicks) and then touch-tone phones (specific pairs of pitches that could be detected by equipment).

This is not a new idea for host/terminal communication; it's just arguably more abstract than most of its analogs. ANSI escape sequences are already used to signal mode changes (like changing text color or weight) or for marking hyperlinks (the OSC 8 standard we've implemented in another PR). Here it's more like dipping out of the stream of text to deliver commentary, as an author or translator might do in prose with brackets.

But because it's communicating genuine metadata and not just superficial things like style, security has to be considered. Because the communication is in-band, there is no obvious way to distinguish between a shell sending metadata… and a nefarious output stream trying to impersonate that shell. Simply catting a file could emit the escape sequences that would open and close that metadata channel. Just as it was with analog phones — tones emitted by phones could be impersonated by 2600hz blue-boxes if you felt like calling someone long-distance but didn’t feel like paying for it.

And some of that data is sensitive, or could be used to make malicious code seem more benign (e.g., by reporting that it's running a simple, innocuous command when it's actually deleting files or reading sensitive data).

The way around this is with a nonce — a one-time secret known to both sides of the communication. This is where the phone analogy falls down, but imagine:

  • Alice wants to send Bob a voice message, but
  • Bob can’t tell Alice's voice apart from Mallory's; and
  • Mallory is not a nice person and is not above pretending to be Alice to get Bob to do something.

The solution is to have Alice and Bob agree ahead of time on some sort of shared secret, like a code word. Whenever Bob receives a message and is unsure who sent it, he knows that if the code word is present, it’s from Alice. If it’s not present, it’s not to be trusted. This solution is only possible because Mallory can’t hear what Alice says to Bob, or else she’d be able to overhear the code word.

The program sends its data to the host, but it cannot overhear whatever extra data the host sends to the terminal. So important pieces of metadata are marked with a nonce as a way of proving their authenticity. Not everything in OSC 633 carries a nonce; but if it doesn’t, you can assume it’s not sensitive enough to warrant it.

There are other things that we might care about picking up from OSC 633. The most salient one is the ability to spy on environment variables and be notified when they change. This is a big deal for tools like virtualenv and can magnify signals like “which version of Python is running right now” or “which AWS profile is active.”

Another is being able to mark boundaries of the output semantically. If you know where the prompt starts and ends, then you can exclude it from drag-selection of text and make it easier to copy actual output. (Same with zsh’s RPROMPT, whose text often finds its way into Claude Code sessions at work because I copy terminal output straght from iTerm 2. Presumably if I copied from VS Code, it would ignore the RPROMPT! Yeah, this is a feature worth emulating eventually.)

But those can wait! The goal is to keep this PR comprehensible and ship a feature that is useful on its own but also unblocks other features (like path detection).

Testing the machinery

The penance for offloading a feature to a robot is to be thorough about tests. There are three parts that make this work:

  1. Shell scripts (for four different shell environments) designed to be evaluated in between each command in order to send metadata over the stream.
  2. An XTerm addon that can pluck the OSC 633 messages out of the stream and forward them to interested parties.
  3. The terminal extension to introduce the two parts to one another — recognizing when the user’s shell is an injectable shell and ensuring that that shell is started up in such a way that its own “middleware” can be injected (but otherwise be seamless to the user).

The second and third parts have thorough test coverage, though each one in isolation. (The XTerm addon is easy to test; you can write data to a terminal and then test that the data is read as expected by the addon.) The scripts themselves — written, as they are, in various shell script dialects — don’t have any test coverage, nor do any of the tests attempt to establish a genuine terminal session with a PTY. Which is fine; no way to set that up such that the specs can be run fron anyone’s machine!

One way around this might be to introduce some Playwright (end-to-end) tests eventually. We use Playwright for our post-build smoke tests; in this case we could write four Playwright test suites (one for each integration) and run it on GitHub Actions in an appropriate runner for the integration being tested.

But this can be a future thing! The feature is easy enough to get working and is a good foundation for what is to come, and the impact of it possibly not working correctly is not far from the status quo. For now, this means that your cwd will be updated as it changes; therefore:

  • When you restart Pulsar or reload your window, any restored terminal session should return to the last cwd it used, rather than the one it had when you opened it.
  • Likewise for splitting a pane; the duplicate pane will inherit the current cwd, not the initial cwd.
  • If you invoke tree-view:reveal-active-file, the active folder in the project should be highlighted in the tree view (if cwd is within the project).

Please test this manually! Especially if you use a non-standard shell like fish, or if you are a Windows user. I know it works for me with zsh, but I haven’t tested the other integrations yet.

@savetheclocktower savetheclocktower linked an issue Aug 20, 2026 that may be closed by this pull request
5 tasks
@savetheclocktower savetheclocktower added this to the 1.133.0 milestone Aug 20, 2026
@savetheclocktower
savetheclocktower force-pushed the shell-integration-for-cwd branch from 86b293e to 3a5040d Compare August 21, 2026 06:21
@savetheclocktower
savetheclocktower changed the base branch from main to hyperlinks August 21, 2026 06:22
@savetheclocktower
savetheclocktower force-pushed the shell-integration-for-cwd branch from 3a5040d to e9fd34c Compare August 21, 2026 06:33
…for bash, zsh, fish, and PowerShell.

Implements OSC 633, opening the door for further enhancements; but the main value is in tracking `cwd` so we can update the pane item's path and lay the groundwork for proper interpretation of relative paths.
@savetheclocktower
savetheclocktower force-pushed the shell-integration-for-cwd branch from e9fd34c to 1335163 Compare August 21, 2026 06:56
# Conflicts:
#	lib/.build-hash
#	spec/element-spec.js
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.

Integrate with shell in order to track cwd

1 participant