Skip to content

fix: apply Ghostty terminal colors via AppleScript from TTY-less subprocesses - #3

Open
mattschwarz wants to merge 1 commit into
basicScandal:mainfrom
mattschwarz:feat/ghostty-applescript-colors
Open

fix: apply Ghostty terminal colors via AppleScript from TTY-less subprocesses#3
mattschwarz wants to merge 1 commit into
basicScandal:mainfrom
mattschwarz:feat/ghostty-applescript-colors

Conversation

@mattschwarz

Copy link
Copy Markdown

Summary

Problem

Claude Code runs hook/tool subprocesses without a controlling terminal (deliberate, as of v2.1.139). Both standard color-delivery paths fail:

  • /dev/ttyDevice not configured (no controlling terminal in the subprocess)
  • OSC via stdout — Claude Code's TUI renderer strips the leading ESC byte before output reaches the emulator, so sequences arrive as inert text

This is the same root cause #2 fixes for iTerm2. This PR covers Ghostty.

Solution

Ghostty (1.3.1) ships an AppleScript dictionary (NSAppleScriptEnabled, Ghostty.sdef) exposing a perform action command. osascript reaches Ghostty over Apple Events, bypassing the PTY entirely, so it works from any subprocess regardless of TTY availability.

Ghostty's dictionary has no per-session color properties (unlike iTerm2), so colors are applied by writing them into Ghostty's config file and reloading:

osascript -e 'tell application "Ghostty" to perform action "reload_config" on (terminal 1 of front window)'
  • activate.sh writes background, foreground, cursor-color, and the ANSI palette = 0..7 into a delimited managed block in ${XDG_CONFIG_HOME:-$HOME/.config}/ghostty/config, preserving any user config outside the markers, then reloads.
  • deactivate.sh strips the managed block (removing the file entirely if it held only our block) and reloads, restoring Ghostty defaults.

Files changed

  • engine/activate.sh — write managed Ghostty color block + AppleScript reload
  • engine/deactivate.sh — remove managed block + AppleScript reload on reset/exit

Testing

  1. Open Claude Code in Ghostty on macOS.
  2. /skin mission-control — background → deep navy (#0B1120), foreground → amber (#FFB000), immediately.
  3. /skin nebula — background → deep purple (#0E0520).
  4. /skin reset — colors restored to Ghostty defaults.

Out-of-band path directly (works with no TTY attached):

osascript -e 'tell application "Ghostty" to perform action "reload_config" on (terminal 1 of front window)'

Known limitations / notes

  • macOS + Ghostty only. Guarded on uname == Darwin and Ghostty env detection (TERM_PROGRAM=ghostty / GHOSTTY_RESOURCES_DIR). Other terminals: silent no-op.
  • Colors are global. Ghostty's config applies to all windows/surfaces, so every Ghostty window recolors — not just the Claude session's surface. Ghostty's dictionary exposes no per-surface color properties (iTerm2 does, which is why fix: apply iTerm2 terminal colors via AppleScript from Claude Code hook subprocesses #2 can scope per session).
  • Requires the standard macOS Automation permission to control Ghostty (granted once via the TCC prompt).
  • The managed block is delimited by # >>> claude-skins / # <<< claude-skins; anything outside the markers is preserved.

🤖 Generated with Claude Code

…rocesses

Claude Code (v2.1.139+) runs hook/tool subprocesses with no controlling terminal, so OSC color sequences to /dev/tty and via stdout are both dropped. Ghostty is AppleScript-scriptable: write the skin palette into its config file (delimited managed block) and trigger an out-of-band reload_config over Apple Events, which needs no TTY. No-op on non-Ghostty terminals and non-macOS.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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