Fixes#9868 (shift+backspace part only)
You have to use `fish_key_reader -V` to verify this since it uses
different modes than Kitty's key reader. This matches the encoding to
Kitty.
Due to security issues, `sudo` implementations may not preserve
environment variables unless appended with `--preserve-env=list`.
Signed-off-by: definfo <hjsdbb1@gmail.com>
Tried my hand at #8432
Currently lacking tests and some sort of visual indicator that the
surface is locked.
Also, not entirely sure if I needed to touch `application.zig`.
Found what needed changing with help from Copilot (and added Docs w/
Copilot), but wrote most of the code myself.
(Claude-generated content below)
## Fix bash shell integration use-after-free bug
### Problem
After updating to the latest `main` branch, Ghostty fails to launch with
bash shell integration enabled on Linux (Ubuntu 25.10). The terminal
window briefly appears and then closes with an error like:
```
/bin/sh: 1: ically: not found
```
The error message varies but typically contains fragments of words (like
"ically" from "automatically").
Related issue was possibly [reported in the comments of PR
#9881](https://github.com/ghostty-org/ghostty/pull/9881) shortly after
it was merged.
### Root Cause Analysis
The bug is a **use-after-free** in the bash shell integration setup.
In `setupBash()`, the `ShellCommandBuilder` is initialized with a
`stackFallback` allocator:
```zig
var stack_fallback = std.heap.stackFallback(4096, alloc);
var cmd = internal_os.shell.ShellCommandBuilder.init(stack_fallback.get());
```
When `cmd.toOwnedSlice()` is called, it returns a slice allocated from
the `stackFallback` allocator. Since the command string (e.g., "bash
--posix") easily fits within 4096 bytes, this memory is allocated on the
**stack**.
When `setupBash()` returns, the stack frame is deallocated, but the
returned `.shell` command still points to this now-invalid stack memory.
When the command is later used in `execCommand()` to build the `/bin/sh
-c "..."` invocation, it reads garbage data.
The garbage data often contains "ically" because it picks up fragments
from nearby memory—likely from the string literal "automatically" in the
comment on line 280:
```zig
// being manually sourced or automatically injected (from here).
```
This is a [known footgun with Zig's stackFallback
allocator](https://github.com/ziglang/zig/issues/16344) - memory
allocated from the stack portion becomes invalid once the stack frame is
exited.
### Bisect / Blame
The bug was introduced **~5 hours ago** (earlier today):
| Field | Value |
|-------|-------|
| **Commit** | `04fecd7c07fccad423ab1c33324a1997e142b6e2` |
| **PR** | [#9881](https://github.com/ghostty-org/ghostty/pull/9881)
(os/shell: introduce ShellCommandBuilder) |
| **Date** | Fri Dec 12 08:59:44 2025 -0500 |
| **Merged as** | `dd06d8a13` |
The previous implementation in commit `c0deaaba4` used
`std.mem.joinZ(alloc, " ", args.items)` which correctly allocated the
result using the arena allocator. The refactor to use
`ShellCommandBuilder` inadvertently changed the allocation strategy to
use stack memory for the returned value.
Other uses of `stackFallback` in the codebase are safe because they
either:
- Copy data to another allocator before the function returns (e.g.,
`env.put()` in `setupXdgDataDirs`)
- Use the data only within the function scope (e.g.,
`stream_handler.zig`)
### The Fix
Copy the command string to the arena allocator before returning:
```zig
const cmd_str = try cmd.toOwnedSlice();
return .{ .shell = try alloc.dupeZ(u8, cmd_str) };
```
This ensures the memory remains valid for the lifetime needed by the
caller, matching the behavior of other `stackFallback` usage patterns in
the codebase.
### Testing
- Verified the fix on Ubuntu 25.10 with Ghostty built from source
- Before fix: Terminal crashes immediately with "ically: not found"
error
- After fix: Terminal launches successfully with shell integration
working
```
info(io_exec): shell integration automatically injected shell=.bash
info(io_exec): started subcommand path=/bin/sh pid=...
info(surface): surface closed addr=... # Normal close, no "abnormal process exit"
```
### Platform
Primarily affects Linux where `.shell` commands are wrapped in `/bin/sh
-c "..."`. macOS uses a different execution path via `login(1)`.
The ShellCommandBuilder uses a stackFallback allocator, which means
toOwnedSlice() may return memory allocated on the stack. When setupBash()
returns, this stack memory becomes invalid, causing a use-after-free.
This manifested as garbage data in the shell command string, often
appearing as errors like "/bin/sh: 1: ically: not found" (where "ically"
was part of nearby memory, likely from the comment "automatically").
The fix copies the command string to the arena allocator before returning,
ensuring the memory remains valid for the lifetime of the command.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This builder is an efficient way to construct space-separated shell
command strings.
We use it in setupBash to avoid using an intermediate array of arguments
to construct our bash command line.
I think at this point all moderators and helpers can agree with me in
that LLM-generated responses are a blight upon this Earth.
Also probably worth putting in a clause against AI-generated assets
This builder is an efficient way to construct space-separated shell
command strings.
We use it in setupBash to avoid using an intermediate array of arguments
to construct our bash command line.
I think at this point all moderators and helpers can agree with me in
that LLM-generated responses are a blight upon this Earth.
Also probably worth putting in a clause against AI-generated assets
(cf. the Commit Goods situation)
This adds the ability to change a _tab_ title. The previous
functionality was tied to a specific _surface_. A tab title will stick
to the current tab regardless of active splits and so on.
This follows the nomenclature that macOS terminal app does which is
"title vs terminal title" (although we explicitly use "tab" in various
places, I may remove that in the future).
**This is macOS only. GTK is tracked here: #9880**. I did macOS only
because thats the machine I'm on. It'll be trivial to add this to GTK,
too.
## Demo
https://github.com/user-attachments/assets/d9446785-d919-4212-8553-db50c56c8c2f
(The option is also in the main menu, the context menu, and the command
palette)
## AI Disclosure
This PR was done fully with Amp, I didn't write a single line of code at
the time of writing this PR description. I reviewed everything though
and fully understand it all. Its a mimic more or less of the prompt
surface title work (although we did unify some stuff like the apprt
action).
Prior to #7044, on macOS, our shell-integrated command line would be
executed under `exec -l`, which causes bash to be started as a login
shell. This matches the macOS platform norms.
The change to direct command execution meant that we'd skip that path,
and bash would start as a normal interactive (non-login) shell on macOS.
We fixed this in #7253 by adding `--login` to the `bash` direct command
on macOS.
This avoided some of the overhead of starting an extra process just to
get a login shell, but it unfortunately doesn't quite match the bash
environment we get when shell integration isn't enabled (namely, `$0`
doesn't get the login-shell-identifying `-` prefix).
Instead, this change implements the approach proposed in #7254, which
switches the bash shell integration path to use a `.shell` command,
giving us the same execution environment as the non-shell-integrated
command.