#!/bin/zsh -l # gui-launch.sh — matrix-bridge GUI launcher (Approach B: "open a real desktop terminal"). # # Invoked over SSH by the bot: gui-launch.sh # # Opens a Terminal.app window on the logged-in desktop that runs launch-claude.sh, so # `claude` runs INSIDE the GUI session: it gets the login Keychain (no long-lived token # needed) and a real TTY. Claude Code Remote Control then surfaces the session to the phone. # # Requires: the Mac logged into its desktop, and a one-time "Allow ssh to control # Terminal" Automation grant (System Settings > Privacy & Security > Automation). # # Layering: gui-launch.sh owns the GUI/session seam; launch-claude.sh owns the # environment seam (cd + exec claude). Keep that split. script_dir="${0:A:h}" inner="$script_dir/launch-claude.sh" repo_dir="$1" shift prompt="$*" if [[ -z "$repo_dir" || -z "$prompt" ]]; then print -u2 "usage: gui-launch.sh " exit 2 fi # Fail loud on a bad directory — never open a session in the wrong place (guardrail). if [[ ! -d "$repo_dir" ]]; then print -u2 "gui-launch: no such repo dir: $repo_dir" exit 1 fi # Embed repo+prompt in a throwaway script via %q so user text NEVER crosses the # AppleScript string layer — only a safe mktemp path does. This sidesteps the # SSH -> osascript -> shell -> wrapper multi-shell quoting trap the spec warns about. launch_script="$(mktemp -t mb-launch)" { print -r -- '#!/bin/zsh -l' # Propagate a caller-supplied session name (the bot sets this for all-projects launches). [[ -n "$MB_SESSION_NAME" ]] && printf 'export MB_SESSION_NAME=%q\n' "$MB_SESSION_NAME" printf 'exec %q %q %q\n' "$inner" "$repo_dir" "$prompt" } >| "$launch_script" chmod +x "$launch_script" # mktemp paths contain no spaces/quotes, so embedding the path directly is safe. osascript -e "tell application \"Terminal\" to do script \"$launch_script\"" \ -e 'tell application "Terminal" to activate' print -- "gui-launch: opened Terminal for $repo_dir"