cd /news/ai-tools/ghostty-agent-restore-after-a-reboot… · home › topics › ai-tools › article
[ARTICLE · art-152564] src=gist.github.com ↗ pub= topic=ai-tools verified=true sentiment=↑ positive

ghostty-agent-restore: after a reboot or Ghostty restart, every Ghostty tab automatically resumes the Claude Code, Codex or omp session it was running (macOS, zsh)

A developer released ghostty-agent-restore, a zsh script for macOS that snapshots every Ghostty terminal pane's working directory and running agent session, then automatically resumes Claude Code, Codex or omp sessions by session ID after a reboot, crash, quit or update. The tool uses a launchd timer to snapshot panes every two minutes and a .zshrc hook to reattach each new pane to its saved entry, falling back to typing non-agent commands onto the prompt for manual execution.

by read22 min views1 publishedOct 17, 2026

| | #!/bin/zsh | | | # ghostty-agent-restore: remember what every Ghostty pane was doing, and put it back. | | | # | | | # snapshot record every pane: tty, terminal id, tab position, cwd, how to resume it (launchd: every 2 min) | | | # list show the last snapshot | | | # restart snapshot, quit Ghostty and relaunch it; every pane resumes its session | | | # arm restore the last snapshot again: panes whose shell starts in the next 15 min take their entry | | | # claim (run by the .zshrc hook) learn this pane's terminal id; print what to run in it | | | # hook print the zsh code the .zshrc line evals | | | # install launchd timer + .zshrc hook | | | # | | | # Restore starts by itself whenever the running Ghostty is not the one the last snapshot was | | | # taken under: after a reboot, a quit, a crash or an update. When Ghostty restores its | | | # windows (window-save-state = always), each terminal keeps its id and gets back the session | | | # saved under it. When it doesn't, each new tab you open takes the next session, in the | | | # original tab order. omp, Claude Code and Codex sessions are resumed by session id and run; | | | # any other command is typed onto the prompt for you to run. | | | | | | set -u | | | emulate -L zsh | | | setopt extended_glob | | | | | | DIR="${GHOSTTY_AGENT_RESTORE_DIR:-$HOME/.config/ghostty/agent-restore}" | | | CURRENT="$DIR/current.tsv" # tty, terminal id, position, cwd, mode (run | type | empty for a shell), command | | | RESTORE="$DIR/restore.tsv" # entries still waiting for their pane | | | INSTANCE="$DIR/instance" # "<pid> <start time>" of the Ghostty that current.tsv describes | | | LAYOUT="$DIR/layout.tsv" # terminal id → "window.tab.split" position, as of the last pane to open | | | PANES="$DIR/panes" # <tty> → "<terminal id><TAB><pane shell pid>", written by each pane's claim | | | LOCK="$DIR/.claim.lock" | | | FRESH_SECONDS=900 | | | GHOSTTY_BIN='/Applications/Ghostty.app/Contents/MacOS/ghostty' |

|  | LABEL="com.$(whoami).ghostty-agent-restore" | 
|  | OMP_DIR="${PI_CODING_AGENT_DIR:-$HOME/.omp/agent}" | 
|  | CLAUDE_DIR="${CLAUDE_CONFIG_DIR:-$HOME/.claude}" | 
|  | CODEX_DIR="${CODEX_HOME:-$HOME/.codex}" | 

| | | | | # Per agent: options that take a value, options a resume must not repeat, and subcommands | | | # that are not an interactive session. From --help of omp 18.10, Claude Code 2.1.283 and | | | # Codex 0.153. | | | OMP_TAKES='--model --smol --slow --plan --goal --prewalk-into --plan-yolo-into --provider --api-key --system-prompt --system-prompt-template --append-system-prompt --profile --alias --cwd --mode --config --add-dir -r --resume --session-dir --models --tools --thinking --service-tier --hook -e --extension --skills --export --max-time --approval-mode --plugin-dir' | | | OMP_DROP='-r --resume -c --continue --goal --plan-yolo --plan-yolo-into' | | | CLAUDE_TAKES='--add-dir --agent --agents --allowedTools --allowed-tools --append-system-prompt --append-system-prompt-file --autocompact --betas --client-data-url --debug-file --disallowedTools --disallowed-tools --effort --environment --fallback-model --file --input-format --json-schema --max-budget-usd --mcp-config --model -n --name --output-format --permission-mode --permission-prompts --plugin-dir --plugin-url --remote-control-session-name-prefix --setting-sources --settings --system-prompt --system-prompt-file --system-prompt-snapshot --tools -r --resume --session-id --from-pr --teleport' | | | CLAUDE_DROP='-r --resume -c --continue --session-id --fork-session --from-pr --teleport' | | | CLAUDE_COMMANDS='agents attach auth auto-mode doctor gateway import install logs mcp plugin plugins purge respawn rm setup-token stop kill ultrareview update upgrade' |

|  | CODEX_TAKES='-c --config --enable --disable --remote --remote-auth-token-env -i --image -m --model --local-provider -p --profile -s --sandbox -C --cd --add-dir -a --ask-for-approval' | 
|  | CODEX_DROP='--last --all --include-non-interactive' | 

| | CODEX_COMMANDS='exec e review login logout mcp mcp-server app-server remote-control app completion update doctor sandbox debug apply a cloud exec-server features help archive delete unarchive migrate-rollouts agents plugin queue' | | | | | | mkdir -p "$DIR" "$PANES" | | | |

|  | ghostty_pid() { | 
|  | /bin/ps -axo pid=,comm= \| /usr/bin/awk -v b="$GHOSTTY_BIN" '$2==b {print $1; exit}' | 

| | } | | | |

|  | ghostty_instance() {  # "<pid> <start time>" of the running Ghostty, if any | 
|  | local pid=$(ghostty_pid) | 
|  | [[ -n $pid ]] \|\| return 0 | 
|  | local s=$(LC_ALL=C /bin/ps -o lstart= -p "$pid") | 
|  | print -r -- "$pid ${(j: :)${=s}}" | 

| | } | | | |

|  | proc_start() {  # $1 = pid → its start time in epoch seconds | 
|  | local s=$(LC_ALL=C /bin/ps -o lstart= -p "$1" 2>/dev/null) | 
|  | [[ -n $s ]] && LC_ALL=C /bin/date -j -f '%a %b %d %T %Y' "${(j: :)${=s}}" +%s 2>/dev/null | 

| | } | | | | | | # The interactive shell of a pane is the child of Ghostty's login process. |

|  | pane_shells() { | 
|  | local gpid=$(ghostty_pid) | 
|  | [[ -z $gpid ]] && return 1 | 
|  | /bin/ps -axo pid=,ppid= \| /usr/bin/awk -v g="$gpid" '$2==g {print $1}' \| while read -r login; do | 
|  | local sh=$(/bin/ps -axo pid=,ppid= \| /usr/bin/awk -v p="$login" '$2==p {print $1; exit}') | 
|  | print -r -- "${sh:-$login}" | 

| | done | | | } | | | |

|  | foreground_job() {  # $1 = shell pid → "pid argv" of its foreground job, if any | 
|  | /bin/ps -axo pid=,ppid=,stat=,command= \| /usr/bin/awk -v s="$1" '$2==s && $3 ~ /\+/ {p=$1; sub(/^ *[0-9]+ +[0-9]+ +[^ ]+ +/, ""); print p " " $0; exit}' | 

| | } | | | | | | # Splits an agent's argv into the options a resume keeps (with their values), the first | | | # positional word (a subcommand, or the start of a prompt or an old session id; positionals | | | # are never kept, so a prompt is not sent again) and every option seen. |

|  | typeset -ga KEPT; typeset -g FIRST='' SEEN=' ' | 
|  | parse_agent() {  # $1 = options that take a value, $2 = options to drop, rest = argv | 
|  | local takes=" $1 " drops=" $2 " a name; integer value; shift 2 | 
|  | KEPT=() FIRST='' SEEN=' ' | 
|  | while (( $# )); do | 
|  | a=$1; shift | 
|  | if [[ $a == -?* ]]; then | 
|  | name=${a%%=*}; SEEN+="$name "; value=0 | 
|  | [[ $a != *=* && $takes == *" $name "* && $# -gt 0 && $1 != -* ]] && value=1 | 
|  | if [[ $drops == *" $name "* ]]; then | 
|  | (( value )) && shift | 

| | else | | | KEPT+=("$a"); (( value )) && { KEPT+=("$1"); shift } | | | fi | | | elif [[ -z $FIRST ]]; then | | | FIRST=$a | | | fi | | | done | | | } | | | seen() { local f; for f; do [[ $SEEN == " $f " ]] && return 0; done; return 1 } | | | | | | # omp keeps a registry per terminal: line 1 the session's cwd, line 2 its session file. |

|  | resume_omp() {  # $1 = tty, $2 = cwd, rest = argv after `omp` → "cwd<TAB>command", or nothing | 
|  | local cwd=$2 reg="$OMP_DIR/terminal-sessions/$1"; shift 2 | 

| | parse_agent "$OMP_TAKES" "$OMP_DROP" "$@" |

|  | seen -p --print --export --no-session --mode -h --help && return 0 | 
|  | [[ -r $reg ]] \|\| return 0 | 
|  | local sess_cwd=$(/usr/bin/sed -n 1p "$reg") | 
|  | local id=$(/usr/bin/sed -n 2p "$reg" \| /usr/bin/sed -n 's#.*_$[0-9a-f-]\{36\}$\.jsonl$#\1#p') | 
|  | [[ -n $id ]] \|\| return 0 | 
|  | local -a cmd; cmd=(omp "${(@q)KEPT}" --resume "$id") | 
|  | print -r -- "${sess_cwd:-$cwd}"$'\t'"${(j: :)cmd}" | 

| | } | | | | | | # Claude Code 2.1 keeps ~/.claude/sessions/<pid>.json with the live session id of each running | | | # claude; it follows /clear and /resume. Older versions: the newest transcript in the folder | | | # written since that claude started. |

|  | resume_claude() {  # $1 = cwd, $2 = pid, rest = argv after `claude` → "cwd<TAB>command", or nothing | 
|  | local cwd=$1 pid=$2 state="$CLAUDE_DIR/sessions/$2.json" id='' start; local -a t; shift 2 | 

| | parse_agent "$CLAUDE_TAKES" "$CLAUDE_DROP" "$@" |

|  | if [[ -r $state ]]; then | 
|  | local kind=$(/usr/bin/sed -n 's/.*"kind":"$[^"]*$".*/\1/p' "$state") | 
|  | [[ -z $kind \|\| $kind == interactive ]] \|\| return 0 | 
|  | id=$(/usr/bin/sed -n 's/.*"sessionId":"$[0-9a-f-]\{36\}$".*/\1/p' "$state") | 
|  | [[ -n $id ]] && t=($CLAUDE_DIR/projects/*/$id.jsonl(N)) | 
|  | (( $#t )) \|\| id='' # nothing said in it yet: start fresh | 

| | else |

|  | seen -p --print --bg --background -h --help -v --version && return 0 | 
|  | [[ -n $FIRST && " $CLAUDE_COMMANDS " == *" $FIRST "* ]] && return 0 | 
|  | start=$(proc_start "$pid") | 
|  | t=($CLAUDE_DIR/projects/${${cwd:A}//[^A-Za-z0-9]/-}/*.jsonl(Nom[1])) | 
|  | (( $#t )) && (( $(/usr/bin/stat -f %m "$t[1]") >= ${start:-0} )) && id=${${t[1]:t}%.jsonl} | 

| | fi |

|  | local -a cmd; cmd=(claude "${(@q)KEPT}"); [[ -n $id ]] && cmd+=(--resume "$id") | 
|  | print -r -- "$cwd"$'\t'"${(j: :)cmd}" | 

| | } | | | | | | # Codex writes each session to ~/.codex/sessions/.../rollout-<time>-<id>.jsonl and indexes | | | # it in the threads table of ~/.codex/state_<n>.sqlite. | | | codex_session() { # $1 = codex pid, $2 = cwd → id of the session it is writing, if found | | | local pids=$1 child id |

|  | for child in $(/usr/bin/pgrep -P "$1"); do pids+=",$child"; done | 
|  | id=$(/usr/sbin/lsof -p "$pids" -Fn 2>/dev/null \| /usr/bin/sed -n 's#^n.*/rollout-.*-$[0-9a-f]\{8\}-[0-9a-f]\{4\}-[0-9a-f]\{4\}-[0-9a-f]\{4\}-[0-9a-f]\{12\}$\.jsonl$#\1#p' \| /usr/bin/head -1) | 
|  | if [[ -z $id ]]; then # not held open: the thread in this folder updated since this codex started | 
|  | local -a db; db=($CODEX_DIR/state_*.sqlite(Nom[1])) | 
|  | local start=$(proc_start "$1") a=${2//"'"/"''"} b=${${2:A}//"'"/"''"} | 
|  | (( $#db )) && [[ -n $start ]] && id=$(/usr/bin/sqlite3 -readonly "$db[1]" \ | 
|  | "select id from threads where cwd in ('$a', '$b') and updated_at_ms >= ${start}000 and archived = 0 order by updated_at_ms desc limit 1" 2>/dev/null) | 

| | fi | | | print -r -- "$id" | | | } | | | |

|  | resume_codex() {  # $1 = cwd, $2 = pid, rest = argv after `codex` → "cwd<TAB>command", or nothing | 
|  | local cwd=$1 pid=$2; shift 2 | 

| | parse_agent "$CODEX_TAKES" "$CODEX_DROP" "$@" |

|  | seen -h --help -V --version && return 0 | 
|  | [[ -n $FIRST && " $CODEX_COMMANDS " == *" $FIRST "* ]] && return 0 | 
|  | local id=$(codex_session "$pid" "$cwd") | 
|  | local -a cmd; cmd=(codex); [[ -n $id ]] && cmd+=(resume "$id"); cmd+=("${(@q)KEPT}") | 
|  | print -r -- "$cwd"$'\t'"${(j: :)cmd}" | 

| | } | | | | | | # How to bring a pane back. An agent session becomes a resume command that runs by itself; | | | # any other foreground command (ssh, vim, a dev server) is typed onto the prompt, not run. |

|  | resumable_command() {  # $1 = tty, $2 = cwd, $3 = foreground pid, $4 = its argv → "cwd<TAB>mode<TAB>command" | 
|  | local tty=$1 cwd=$2 pid=$3 fg=$4 out='' | 
|  | [[ $fg == *ghostty-agent-restore* ]] && fg='' # never record this tool itself: `restart` would loop | 
|  | [[ -n $fg ]] \|\| { print -r -- "$cwd"$'\t\t'; return } | 
|  | local -a argv; argv=(${(s: :)fg})   # ps joins argv with spaces and keeps no quoting | 
|  | [[ ${argv[1]:t} == (node\|bun) && ${${argv[2]:-}:t} == (omp\|claude\|codex) ]] && shift argv | 
|  | case ${argv[1]:t} in | 
|  | omp)    out=$(resume_omp "$tty" "$cwd" "${(@)argv[2,-1]}") ;; | 
|  | claude) out=$(resume_claude "$cwd" "$pid" "${(@)argv[2,-1]}") ;; | 
|  | codex)  out=$(resume_codex "$cwd" "$pid" "${(@)argv[2,-1]}") ;; | 

| | esac |

|  | if [[ -n $out ]]; then | 
|  | print -r -- "${out%%$'\t'*}"$'\trun\t'"${out#*$'\t'}" | 

| | else | | | print -r -- "$cwd"$'\ttype\t'"$fg" | | | fi | | | } | | | | | | # The terminal id a pane's shell recorded at its first prompt; ignored once that shell is | | | # gone, since a new pane can reuse the tty. |

|  | pane_terminal() {  # $1 = tty, $2 = pane shell pid | 
|  | [[ -r $PANES/$1 ]] \|\| return 0 | 
|  | local -a f; f=("${(@ps:\t:)$(<$PANES/$1)}") | 
|  | [[ ${f[2]:-} == $2 ]] && print -r -- "${f[1]}" | 

| | } | | | | | | lock() { | | | local i | | | for i in {1..50}; do | | | /bin/mkdir "$LOCK" 2>/dev/null && return 0 | | | sleep 0.1 | | | done | | | return 1 | | | } | | | unlock() { /bin/rmdir "$LOCK" 2>/dev/null } | | | | | | # True when current.tsv was taken under a Ghostty that is no longer running: the Mac | | | # rebooted, or Ghostty quit, crashed or updated. |

|  | from_previous_ghostty() {  # $1 = running Ghostty instance | 
|  | [[ -n $1 && -s $CURRENT && -r $INSTANCE && $(<$INSTANCE) != $1 ]] | 

| | } | | | | | | # Caller holds $LOCK. Arms the previous Ghostty's snapshot, once. | | | arm_previous() { # $1 = running Ghostty instance | | | from_previous_ghostty "$1" || return 0 | | | /bin/cp -f "$CURRENT" "$RESTORE" && print -r -- "$1" > "$INSTANCE" | | | } | | | |

|  | cmd_snapshot() { | 
|  | local inst=$(ghostty_instance) | 
|  | [[ -n $inst ]] \|\| return 1   # Ghostty not running: keep the last snapshot | 
|  | local -A position; local i p | 
|  | [[ -r $LAYOUT ]] && while IFS=$'\t' read -r i p; do position[$i]=$p; done < "$LAYOUT" | 

| | local tmp="$CURRENT.tmp.$$" sh tty id cwd job |

|  | : > "$tmp" | 
|  | for sh in $(pane_shells); do | 
|  | tty=$(/bin/ps -o tty= -p "$sh" \| /usr/bin/tr -d ' ') | 
|  | id=$(pane_terminal "$tty" "$sh"); p='' | 
|  | [[ -n $id ]] && p=${position[$id]:-} | 
|  | cwd=$(/usr/sbin/lsof -a -p "$sh" -d cwd -Fn 2>/dev/null \| /usr/bin/sed -n 's/^n//p' \| /usr/bin/head -1) | 
|  | job=$(foreground_job "$sh") | 
|  | print -r -- "$tty"$'\t'"$id"$'\t'"$p"$'\t'"$(resumable_command "$tty" "${cwd:-$HOME}" "${job%% *}" "${job#* }")" >> "$tmp" | 

| | done | | | if [[ ! -s $tmp ]]; then | | | /bin/rm -f "$tmp" # no panes (Ghostty starting or quitting): keep the last snapshot | | | return 1 | | | fi | | | /usr/bin/sort -o "$tmp" "$tmp" | | | lock || { /bin/rm -f "$tmp"; return 1 } | | | arm_previous "$inst" # the snapshot about to be replaced belongs to the previous Ghostty | | | /bin/mv -f "$tmp" "$CURRENT" && print -r -- "$inst" > "$INSTANCE" | | | unlock | | | } | | | |

|  | cmd_list() { | 
|  | local file="${1:-$CURRENT}" | 
|  | [[ -r $file ]] \|\| { print "no snapshot yet; run: ghostty-agent-restore snapshot"; return 1 } | 
|  | print "${file:t} · $(/bin/date -r "$file" '+%b %d %H:%M') · $(/usr/bin/wc -l < "$file" \| /usr/bin/tr -d ' ') panes" | 

| | print " tty window:tab cwd command" |

|  | /usr/bin/sort -t $'\t' -k3,3 -k1,1 "$file" \| /usr/bin/awk -F'\t' '{ | 
|  | split($3, p, "."); where = ($3 == "" ? "-" : (p[1] + 0) ":" (p[2] + 0) (p[3] + 0 > 1 ? "." (p[3] + 0) : "")) | 
|  | cwd = (index($4, ENVIRON["HOME"]) == 1 ? "~" substr($4, length(ENVIRON["HOME"]) + 1) : $4) | 
|  | printf "  %-8s %-11s %-40s %s%s\n", $1, where, cwd, ($6 == "" ? "(shell)" : $6), ($5 == "type" ? "   (typed, not run)" : "")}' | 

| | } | | | |

|  | cmd_arm() { | 
|  | [[ -s $CURRENT ]] \|\| { print "nothing to arm"; return 1 } | 

| | /bin/rm -rf "$LOCK" | | | /bin/cp -f "$CURRENT" "$RESTORE" | | | print "armed for ${FRESH_SECONDS}s: a pane takes its entry when its shell starts (`exec zsh` in an empty pane, or a new tab):" | | | cmd_list "$RESTORE" | | | } | | | | | | # Ghostty answers AppleScript with each terminal's id, position and title, so a shell finds | | | # its own terminal id by setting a unique title and looking for it. Sets TERMINAL_ID and | | | # LIVE_IDS, saves every terminal's position to $LAYOUT, and records this pane for snapshots. |

|  | TERMINAL_ID='' LIVE_IDS=' ' | 
|  | identify() {  # $1 = tty, $2 = pane shell pid | 
|  | local token="ghostty-agent-restore-$$-$RANDOM" out='' i | 
|  | print -n -- $'\e]2;'"$token"$'\a' > /dev/tty | 
|  | for i in {1..10}; do | 

| | # Two bulk reads (one Apple event each) instead of one event per property per terminal: | | | # about 0.1 s instead of 0.5 s with a dozen terminals. Ids are read again afterwards; | | | # if a tab opened or closed in between, the rows could pair up wrong, so retry. | | | out=$(/usr/bin/osascript -e 'with timeout of 3 seconds | | | tell application "Ghostty" | | | set ids to id of terminals of tabs of windows | | | set names to name of terminals of tabs of windows | | | set again to id of terminals of tabs of windows | | | end tell | | | end timeout | | | if again is not equal to ids then return "" | | | set out to "" | | | repeat with wi from 1 to count of ids | | | repeat with ti from 1 to count of item wi of ids | | | set tabIds to item ti of item wi of ids | | | set tabNames to item ti of item wi of names | | | repeat with ki from 1 to count of tabIds | | | set out to out & (item ki of tabIds) & (character id 9) & wi & "." & ti & "." & ki & (character id 9) & (item ki of tabNames) & linefeed | | | end repeat | | | end repeat | | | end repeat | | | return out' 2>/dev/null) || break |

|  | TERMINAL_ID=${${(M)${(f)out}:#*$'\t'$token}%%$'\t'*} | 
|  | [[ -n $TERMINAL_ID ]] && break | 

| | sleep 0.05 | | | done |

|  | print -n -- $'\e]2;'"${PWD/#$HOME/~}"$'\a' > /dev/tty | 
|  | [[ -n $out ]] \|\| return 0 | 
|  | local -a ids; ids=(${${(f)out}%%$'\t'*})   # unquoted: inside "..." the lines would be joined first | 
|  | LIVE_IDS=" ${(j: :)ids} " | 
|  | print -r -- "$out" \| /usr/bin/awk -F'\t' 'NF >= 2 {split($2, p, "."); printf "%s\t%03d.%03d.%03d\n", $1, p[1], p[2], p[3]}' > "$LAYOUT.tmp.$$" \ | 

| | && /bin/mv -f "$LAYOUT.tmp.$$" "$LAYOUT" | | | [[ -n $TERMINAL_ID ]] && print -r -- "$TERMINAL_ID"$'\t'"$2" > "$PANES/$1" | | | } | | | | | | # Take this pane's entry from restore.tsv. A pane without one (Ghostty did not restore the | | | # windows, so this is a new tab) takes the session that came first in the old tab order | | | # among those whose terminal is gone. Prints the entry. | | | take_entry() { | | | lock || return |

|  | local n=$(/usr/bin/awk -F'\t' -v me="$TERMINAL_ID" -v live="$LIVE_IDS" ' | 
|  | me != "" && $2 == me { print NR; mine = 1; exit } | 
|  | $6 != "" && ($2 == "" \|\| index(live, " " $2 " ") == 0) { | 
|  | key = ($3 == "" ? "999" : $3) "\t" $1 | 
|  | if (spare == "" \|\| key < best) { spare = NR; best = key } | 

| | } | | | END { if (!mine && spare != "") print spare }' "$RESTORE" 2>/dev/null) | | | local line='' |

|  | if [[ -n $n ]]; then | 
|  | line=$(/usr/bin/sed -n "${n}p" "$RESTORE") | 

| | /usr/bin/sed "${n}d" "$RESTORE" > "$RESTORE.tmp" && /bin/mv -f "$RESTORE.tmp" "$RESTORE" | | | [[ -s $RESTORE ]] || /bin/rm -f "$RESTORE" | | | fi | | | unlock | | | print -r -- "$line" | | | } | | | | | | # Run by the .zshrc hook at a pane's first prompt, with the pane shell's pid. Learns this | | | # pane's terminal id, arms the previous Ghostty's snapshot if this is a new Ghostty, and | | | # prints "<mode><TAB><command line>" for this pane, if it gets an entry. |

|  | cmd_claim() { | 
|  | local shell=${1:-$PPID} | 
|  | local login=$(/bin/ps -o ppid= -p "$shell" \| /usr/bin/tr -d ' ') | 
|  | [[ $(/bin/ps -o comm= -p "$login") == */login ]] \|\| return 0   # a pane's own shell, not a nested one | 
|  | identify "$(/bin/ps -o tty= -p "$shell" \| /usr/bin/tr -d ' ')" "$shell" | 
|  | local inst=$(ghostty_instance) | 

| | if from_previous_ghostty "$inst" && lock; then | | | arm_previous "$inst" | | | unlock | | | fi |

|  | [[ -r $RESTORE ]] \|\| return 0 | 
|  | local age=$(( $(/bin/date +%s) - $(/usr/bin/stat -f %m "$RESTORE") )) | 
|  | if (( age > FRESH_SECONDS )); then | 
|  | /bin/rm -f "$RESTORE"; return 0 | 

| | fi |

|  | local line=$(take_entry) | 
|  | local -a f; f=("${(@ps:\t:)line}") | 
|  | local cwd=${f[4]:-} mode=${f[5]:-} command=${line#*$'\t'*$'\t'*$'\t'*$'\t'*$'\t'} | 
|  | [[ -n $line && -n $mode && -n $command ]] \|\| return 0 | 

| | # real path, so a directory since moved but still reached through a symlink works |

|  | [[ -d $cwd && ${cwd:A} != ${PWD:A} ]] && command="cd -- ${(q)cwd:A} && $command" | 
|  | print -r -- "$mode"$'\t'"$command" | 

| | } | | | | | | # zsh code for .zshrc. The claim runs in precmd, where p10k's instant prompt has stdin on | | | # /dev/null and stdout on a file, so nothing is run there: the command is put on the first | | | # prompt's command line, and submitted only for an agent session. | | | cmd_hook() { | | | /bin/cat <<'EOF' |

|  | _ghostty_agent_restore_claim() { | 
|  | precmd_functions=(${precmd_functions:#_ghostty_agent_restore_claim}) | 
|  | local out=$(ghostty-agent-restore claim $$) | 
|  | [[ -n $out ]] \|\| return 0 | 
|  | typeset -g _ghostty_agent_restore_mode=${out%%$'\t'*} _ghostty_agent_restore_cmd=${out#*$'\t'} | 

| | zle -N _ghostty_agent_restore_run | | | autoload -Uz add-zle-hook-widget && add-zle-hook-widget line-init _ghostty_agent_restore_run | | | } |

|  | _ghostty_agent_restore_run() { | 
|  | [[ -n ${_ghostty_agent_restore_cmd:-} ]] \|\| return 0 | 

| | BUFFER=$_ghostty_agent_restore_cmd | | | CURSOR=${#BUFFER} | | | unset _ghostty_agent_restore_cmd | | | [[ $_ghostty_agent_restore_mode == run ]] && zle accept-line | | | } | | | precmd_functions+=(_ghostty_agent_restore_claim) | | | EOF | | | } | | | | | | cmd_restart() { | | | cmd_snapshot || { print "Ghostty is not running"; return 1 } | | | cmd_list | | | print "\nQuitting Ghostty (confirm the dialog if it asks). It relaunches and every pane resumes." | | | nohup /bin/zsh -c ' | | | /usr/bin/osascript -e '"'"'tell application "Ghostty" to quit'"'"' >/dev/null 2>&1 | | | for i in {1..120}; do /bin/ps -axo comm= | /usr/bin/grep -qx "'"$GHOSTTY_BIN"'" || break; sleep 0.5; done | | | sleep 1 | | | /usr/bin/open -a Ghostty | | | ' >/dev/null 2>&1 &! | | | } | | | | | | cmd_install() { | | | local plist="$HOME/Library/LaunchAgents/$LABEL.plist" | | | /bin/mkdir -p "$HOME/Library/LaunchAgents" | | | /bin/cat > "$plist" <<EOF | | | <?xml version="1.0" encoding="UTF-8"?> | | | <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> |

|  | <plist version="1.0"><dict> | 
|  | <key>Label</key><string>$LABEL</string> | 
|  | <key>ProgramArguments</key><array><string>/bin/zsh</string><string>$HOME/.local/bin/ghostty-agent-restore</string><string>snapshot</string></array> | 
|  | <key>StartInterval</key><integer>120</integer> | 
|  | <key>RunAtLoad</key><true/> | 
|  | <key>StandardErrorPath</key><string>$DIR/launchd.log</string> | 
|  | </dict></plist> | 

| | EOF | | | /bin/launchctl bootout "gui/$UID/$LABEL" 2>/dev/null | | | /bin/launchctl bootstrap "gui/$UID" "$plist" && print "launchd: snapshot every 2 min ($LABEL)" | | | local hook='command -v ghostty-agent-restore >/dev/null && [[ -n ${GHOSTTY_RESOURCES_DIR:-} ]] && eval "$(ghostty-agent-restore hook)" # ghostty-agent-restore' | | | local rc="$HOME/.zshrc" |

|  | if /usr/bin/grep -q '# ghostty-agent-restore$' "$rc" 2>/dev/null; then | 
|  | /usr/bin/awk -v h="$hook" '/# ghostty-agent-restore$/ {print h; next} {print}' "$rc" > "$rc.tmp.$$" && /bin/cat "$rc.tmp.$$" > "$rc" && /bin/rm -f "$rc.tmp.$$" | 

| | print ".zshrc: hook updated" | | | else | | | print -- "\n# Resume what each Ghostty pane was running before Ghostty or the Mac restarted\n$hook" >> "$rc" | | | print ".zshrc: hook added" | | | fi | | | } | | | |

|  | case "${1:-}" in | 
|  | snapshot) cmd_snapshot ;; | 
|  | list)     cmd_list "${2:-}" ;; | 
|  | restart)  cmd_restart ;; | 
|  | arm)      cmd_arm ;; | 
|  | claim)    cmd_claim "${2:-}" ;; | 
|  | hook)     cmd_hook ;; | 
|  | install)  cmd_install ;; | 
|  | *) /usr/bin/sed -n '2,17p' "$0"; exit 64 ;; | 

| | esac |

── more in #ai-tools 4 stories · sorted by recency
── more on @ghostty 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
→ Live at https://your-agent.zahid.host ✓
Get free account → Pricing
from €0/mo · no card required
LIVE [news/ghostty-agent-restor…] indexed:0 read:22min 2026-10-17 · —