Repository navigation
Make fsfork start a shell in the fork on a terminal, with --print to not - #7
Merged
Merged
Conversation
A process cannot change its parent's working directory, so `fsfork` on a terminal printed a path and left you where you were, to copy it or to have known to type `cd $(fsfork)` instead. Now, when stdin and stdout are terminals, it says where the fork is on stderr, and becomes your shell in the fork, as `confine` does with no command; `exit` comes back. When stdout is captured, it prints the path as before, so `cd $(fsfork)` and `fork=$(fsfork)` work as they did. Stdin has to be a terminal too, as a shell would otherwise run what it reads from there as commands, such as the rest of the lines in `... | while read dir; do fsfork "$dir"; done`. A script run from a terminal is on that terminal too, and would stall in a shell if it does not capture the path, so `--print` always prints it. If the shell cannot be run, the fork is removed, so that failing still leaves neither the fork nor the base. The tests run `fsfork` on a pseudo-terminal, with commands typed to a real `/bin/sh`.
Ghostty sets up its shell integration only in the shell it starts: its `.zshenv` restores `ZDOTDIR`, so a zsh started inside it, such as the one `fsfork` becomes, never reports its directory with OSC 7. Ghostty then goes by what the outer shell said last, the directory forked, and a new tab opened while working in the fork, as in Claude Code, opens there. So `fsfork` reports the fork with OSC 7 on the terminal before it becomes the shell, as a shell does at its prompt. Once the shell exits, the outer one reports its own directory again. It also sets `PWD` to the fork, which the shell otherwise inherits as the directory forked.
penberg
added a commit
that referenced
this pull request
Nov 3, 2026
Unifies what the three tools print: errors, warnings, status lines and usage. Each had grown its own shape; now every message is `prog: subject: problem`, on one line, in lowercase, so that people and agents can read it, and it is still as short as a UNIX tool's. | Case | Before | After | | --- | --- | --- | | name taken | `fsfork: /private/…/.p.x.base: File exists (os error 17)` | `fsfork: p.x: file exists` | | base left over | the same | `fsfork: p.x: its base .p.x.base is left over; remove it` | | missing | `fsfork: nope: No such file or directory (os error 2)` | `fsfork: nope: no such file or directory` | | not a directory | `fsfork: /private/…/p/a: not a directory` | `fsfork: p/a: not a directory` | | not a fork | `fsdiff: /abs/p: no base at /abs/.p.base; is it a fork?` | `fsdiff: .: not a fork: no .p.base next to it` | | bad option | `Unrecognized argument: --bogus`, a blank line, `Run fsfork --help for more information.` | `fsfork: unrecognized argument: --bogus` and the usage line | | conflicting options | `confine: --rm removes a fork, and -d runs the command without one` | `confine: --rm cannot be given with -d, which makes no fork` and the usage line | | no such command (macOS) | `sandbox-exec: execvp() of 'nosuchcmd' failed: …`, exit 71 | `confine: nosuchcmd: command not found`, exit 127 | | no Landlock (Linux) | `confine: confine needs Landlock, …` | `confine: Landlock is not available; it needs Linux 5.19 or later` | - **Capitalization.** The system's error text is lowercased and loses Rust's `(os error N)`, so that it matches our own text. - **Paths.** A path is shown as it was typed. A path the command made itself, such as a fork's, is shown as it would be typed from the working directory: `p.x`, or `../turso.1`. `forked to` still gives the full path, because the fork is named only there, and the command then runs somewhere else. - **Usage errors** print the man page's synopsis after the problem: the single most useful line for fixing the arguments. - **`confine` looks for the command first.** A command that isn't there now exits 127, as the man page says, and is checked before forking, so it leaves no fork. A path with a `/` in it is looked for in the directory the command runs in. - **The git fallback warning** is printed by the command that forked, so from `confine` it now says `confine:` and not `fsfork:`. It shows only the first line of git's error. The message formatting lives in `fsfork::message`, which all three commands share. The lib now depends on `argh`, for parsing. Stacked on #7, since it changes the same `fsfork.rs`. Merge that first. ## Not changed - `--help` output is still argh's (`Usage:`, `Options:`). It goes to stdout and isn't a message. - On Linux, errors from `cp` during a fork are passed through as `cp` prints them. ## Testing `cargo test` and `cargo clippy` pass on macOS. New tests check exact stderr for usage errors in all three commands, for lowercase system errors with paths as typed, for taken names and left-over bases, and that `confine` says a missing command is not found, exits 127 and makes no fork. CI covers Linux. The Landlock message hasn't been seen on a machine without Landlock.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
fsforkon a terminal printed a path and left you where you were. A process cannot change its parent's working directory, so now it starts your shell in the fork instead, asconfinedoes with no command:... | while read dir; do fsfork "$dir"; done.--printis for scripts. A script run from a terminal is on that terminal too, and would stall in a shell if it does not capture the path.fsforkstarts never reports its directory with OSC 7, and a new tab opened while working in the fork, as in Claude Code, opened in the directory forked.fsforknow reports the fork with OSC 7 before it becomes the shell, and setsPWDto it. The outer shell reports its own directory again once the shell exits.Rebased on
main, so it works with-nfrom #8.Not done
demo.tapenow usesfsfork --print wordfreq, so the demo's later commands still run from the parent directory, butdemo.gifanddemo.mp4are not re-recorded and still showfsfork wordfreq.confinerunning a command in a fork has the same problem with new tabs, and is not changed here.Testing
cargo testandcargo clippypass on macOS. The new tests runfsforkon a pseudo-terminal with commands typed to a real/bin/sh, covering all three ways of getting the path, the shell's directory,PWDand exit status, the OSC 7 sent to the terminal, and the failure to run a shell. CI covers Linux. The new-tab fix has not been tried in Ghostty itself.