- 01.Starting Point
- 02.Discovering the Existing App Structure
- 03.Creating the Project Structure
- 04.Investigating Ghostty Scripting for New Tab + Command
- 05.Hammerspoon Approach
- 06.The Fix: Find Ghostty Window by Title
- 07.Timing Issues
- 08.Side Quest: Claude Code 2.1.0 Broken
- 09.Where We Landed
- 10.Takeaways
Ghostty Clauddy — macOS App + Hammerspoon Hotkey
Had a \"Claude Code\" mock app somewhere in `~/Applications` — an AppleScript-based wrapper that launches Ghostty with a custom config. Wanted to:
Starting Point
Had a "Claude Code" mock app somewhere in ~/Applications — an AppleScript-based wrapper that launches Ghostty with a custom config. Wanted to:
- Move it into
~/Codeso it's easy to find and modify - Add a global hotkey that opens a new tab AND starts
claude
The app lived at /Users/joel/Applications/Claude Code.app — a shell script launcher disguised as a macOS app.
Discovering the Existing App Structure
Found it via:
find /Users/joel/Applications -maxdepth 2 -name "*Claude*" -o -name "*claude*"
# /Users/joel/Applications/Claude Code.app
# /Users/joel/Applications/Claude Code Browser.appThe launcher was simple — just exec's Ghostty with a custom config:
#!/bin/bash
exec /Applications/Ghostty.app/Contents/MacOS/ghostty \
--config-file=/Users/joel/.config/ghostty/config-claude-codeThe Ghostty config at ~/.config/ghostty/config-claude-code had:
- Catppuccin Mocha theme (distinct from main terminal's Gruvbox Dark)
- Title set to "Claude Code"
- Working directory:
/Users/joel/Code - Window state: never restore (single session per launch)
Creating the Project Structure
Set up ~/Code/claude-code-app/ (later renamed) with:
ghostty-clauddy/
├── src/
│ ├── launcher.sh # Main launcher script
│ └── config-claude-code # Ghostty config
├── resources/
│ └── AppIcon.icns # Custom icon (~1.4MB)
├── build/
│ └── Claude Code.app/ # Built app bundle
├── build.sh # Build + install script
└── CLAUDE.md
The build.sh does:
- Creates
.appbundle structure inbuild/ - Copies launcher, icon, generates Info.plist
- Symlinks Ghostty config to
~/.config/ghostty/config-claude-code - Symlinks the built app to
~/Applications/Claude Code.app
Key insight: symlinks mean edits take effect immediately without rebuilding.
Investigating Ghostty Scripting for New Tab + Command
Wanted: hotkey that opens new tab AND runs claude. Checked Ghostty's capabilities:
/Applications/Ghostty.app/Contents/MacOS/ghostty --help
# Shows +new-window action, but...
/Applications/Ghostty.app/Contents/MacOS/ghostty +new-window --help
# "Only supported on GTK" — macOS doesn't have IPC for thisChecked for AppleScript support:
osascript -e 'tell application "System Events" to get properties of first application process whose name is "Ghostty"'
# has scripting terminology:falseNo native scripting. The new_tab action exists but can't be triggered externally on macOS, and there's no way to chain "new tab + run command" in a keybind.
Web search confirmed: Ghostty's command config applies to ALL tabs globally, not specific keybinds. No way to do "new tab with specific command" natively.
Hammerspoon Approach
Since Ghostty can't do it natively, use Hammerspoon for:
- Global hotkey detection
- Find/focus the Claude Code window
- Send Cmd+T keystroke
- Type "claude" + Enter
First attempt — looked for app by name:
local claudeCode = hs.application.get("Claude Code")
if claudeCode then
claudeCode:activate()This errored:
attempt to call a nil value (method 'activate')
The problem: "Claude Code.app" just execs into Ghostty. There's no separate process — the launcher script becomes a Ghostty process immediately. So hs.application.get("Claude Code") returns nil (or something weird).
The Fix: Find Ghostty Window by Title
Since Claude Code uses title = Claude Code in its Ghostty config, we can find it by window title:
hs.hotkey.bind({"cmd", "shift"}, "C", function()
local ghostty = hs.application.get("Ghostty")
local claudeWindow = nil
if ghostty then
for _, win in ipairs(ghostty:allWindows()) do
if win:title():find("Claude Code") then
claudeWindow = win
break
end
end
end
if claudeWindow then
claudeWindow:focus()
hs.timer.doAfter(0.1, function()
hs.eventtap.keyStroke({"cmd"}, "T")
hs.timer.doAfter(0.5, function()
hs.eventtap.keyStrokes("claude")
hs.eventtap.keyStroke({}, "return")
end)
end)
else
hs.task.new("/usr/bin/open", function()
hs.timer.doAfter(1.5, function()
hs.eventtap.keyStrokes("claude")
hs.eventtap.keyStroke({}, "return")
end)
end, {"-a", "Claude Code"}):start()
end
end)Timing Issues
First test with shorter delays typed partial text:
~/Code
❯ de
zsh: command not found: de
The shell wasn't ready when Hammerspoon started typing. Bumped delays:
- New tab: 0.2s → 0.5s (shell needs to initialize)
- Cold start: 1s → 1.5s (app + shell initialization)
No way to detect shell readiness without parsing output, so fixed delays are the pragmatic solution.
Side Quest: Claude Code 2.1.0 Broken
During testing, Claude Code auto-updated to 2.1.0 which was broken. Existing sessions were fine (running 2.0.75) but new ones failed.
Found the version structure:
ls -la ~/.local/share/claude/versions/
# 2.0.65, 2.0.71-2.0.76, 2.1.0
ls -la ~/.local/bin/claude
# symlink -> /Users/joel/.local/share/claude/versions/2.1.0Reverted:
ln -sf /Users/joel/.local/share/claude/versions/2.0.75 /Users/joel/.local/bin/claudeDisabled auto-update by adding to ~/.claude/settings.json:
{
"env": {
"DISABLE_AUTOUPDATER": "1"
},
...
}Manual update when ready: claude update
Where We Landed
Project: ~/Code/ghostty-clauddy/
Global hotkey: Cmd+Shift+C anywhere:
- If Claude Code window exists → focus it, new tab, run
claude - If not running → launch app, wait for shell, run
claude
Overhead: ~0.3-0.5s on top of claude's own 1-2s startup. Acceptable.
Future idea discussed but not implemented: Pre-warming tabs. Keep 1-2 tabs with claude already loaded, hotkey cycles to next ready one, background spawns replacement. Would need state tracking in Hammerspoon since Ghostty doesn't expose tab state.
Takeaways
- macOS
.appbundles usingexeclose their identity — the process becomes whatever they exec into. Find windows by title instead of app name. - Ghostty has no IPC on macOS (
+new-windowis GTK-only). Keystroke simulation via Hammerspoon is the workaround. - Shell initialization takes real time. 0.5s is safe for new tabs, 1.5s for cold starts.
- Claude Code versions live in
~/.local/share/claude/versions/, symlinked from~/.local/bin/claude. Easy to rollback. DISABLE_AUTOUPDATER=1in settings.json prevents auto-updates.