{"slug": "ghostty-agent-restore-after-a-reboot-or-ghostty-restart-every-ghostty-tab-the-or", "title": "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)", "summary": "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.", "body_md": "|  | #!/bin/zsh | \n|  | # ghostty-agent-restore: remember what every Ghostty pane was doing, and put it back. | \n|  | # | \n|  | #   snapshot   record every pane: tty, terminal id, tab position, cwd, how to resume it (launchd: every 2 min) | \n|  | #   list       show the last snapshot | \n|  | #   restart    snapshot, quit Ghostty and relaunch it; every pane resumes its session | \n|  | #   arm        restore the last snapshot again: panes whose shell starts in the next 15 min take their entry | \n|  | #   claim      (run by the .zshrc hook) learn this pane's terminal id; print what to run in it | \n|  | #   hook       print the zsh code the .zshrc line evals | \n|  | #   install    launchd timer + .zshrc hook | \n|  | # | \n|  | # Restore starts by itself whenever the running Ghostty is not the one the last snapshot was | \n|  | # taken under: after a reboot, a quit, a crash or an update. When Ghostty restores its | \n|  | # windows (`window-save-state = always`), each terminal keeps its id and gets back the session | \n|  | # saved under it. When it doesn't, each new tab you open takes the next session, in the | \n|  | # original tab order. omp, Claude Code and Codex sessions are resumed by session id and run; | \n|  | # any other command is typed onto the prompt for you to run. | \n|  |  | \n|  | set -u | \n|  | emulate -L zsh | \n|  | setopt extended_glob | \n|  |  | \n|  | DIR=\"${GHOSTTY_AGENT_RESTORE_DIR:-$HOME/.config/ghostty/agent-restore}\" | \n|  | CURRENT=\"$DIR/current.tsv\" # tty, terminal id, position, cwd, mode (run \\| type \\| empty for a shell), command | \n|  | RESTORE=\"$DIR/restore.tsv\" # entries still waiting for their pane | \n|  | INSTANCE=\"$DIR/instance\" # \"<pid> <start time>\" of the Ghostty that current.tsv describes | \n|  | LAYOUT=\"$DIR/layout.tsv\" # terminal id → \"window.tab.split\" position, as of the last pane to open | \n|  | PANES=\"$DIR/panes\" # <tty> → \"<terminal id><TAB><pane shell pid>\", written by each pane's claim | \n|  | LOCK=\"$DIR/.claim.lock\" | \n|  | FRESH_SECONDS=900 | \n|  | GHOSTTY_BIN='/Applications/Ghostty.app/Contents/MacOS/ghostty' | \n|  | LABEL=\"com.$(whoami).ghostty-agent-restore\" | \n|  | OMP_DIR=\"${PI_CODING_AGENT_DIR:-$HOME/.omp/agent}\" | \n|  | CLAUDE_DIR=\"${CLAUDE_CONFIG_DIR:-$HOME/.claude}\" | \n|  | CODEX_DIR=\"${CODEX_HOME:-$HOME/.codex}\" | \n|  |  | \n|  | # Per agent: options that take a value, options a resume must not repeat, and subcommands | \n|  | # that are not an interactive session. From --help of omp 18.10, Claude Code 2.1.283 and | \n|  | # Codex 0.153. | \n|  | 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' | \n|  | OMP_DROP='-r --resume -c --continue --goal --plan-yolo --plan-yolo-into' | \n|  | 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' | \n|  | CLAUDE_DROP='-r --resume -c --continue --session-id --fork-session --from-pr --teleport' | \n|  | 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' | \n|  | 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' | \n|  | CODEX_DROP='--last --all --include-non-interactive' | \n|  | 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' | \n|  |  | \n|  | mkdir -p \"$DIR\" \"$PANES\" | \n|  |  | \n|  | ghostty_pid() { | \n|  | /bin/ps -axo pid=,comm= \\| /usr/bin/awk -v b=\"$GHOSTTY_BIN\" '$2==b {print $1; exit}' | \n|  | } | \n|  |  | \n|  | ghostty_instance() {  # \"<pid> <start time>\" of the running Ghostty, if any | \n|  | local pid=$(ghostty_pid) | \n|  | [[ -n $pid ]] \\|\\| return 0 | \n|  | local s=$(LC_ALL=C /bin/ps -o lstart= -p \"$pid\") | \n|  | print -r -- \"$pid ${(j: :)${=s}}\" | \n|  | } | \n|  |  | \n|  | proc_start() {  # $1 = pid → its start time in epoch seconds | \n|  | local s=$(LC_ALL=C /bin/ps -o lstart= -p \"$1\" 2>/dev/null) | \n|  | [[ -n $s ]] && LC_ALL=C /bin/date -j -f '%a %b %d %T %Y' \"${(j: :)${=s}}\" +%s 2>/dev/null | \n|  | } | \n|  |  | \n|  | # The interactive shell of a pane is the child of Ghostty's `login` process. | \n|  | pane_shells() { | \n|  | local gpid=$(ghostty_pid) | \n|  | [[ -z $gpid ]] && return 1 | \n|  | /bin/ps -axo pid=,ppid= \\| /usr/bin/awk -v g=\"$gpid\" '$2==g {print $1}' \\| while read -r login; do | \n|  | local sh=$(/bin/ps -axo pid=,ppid= \\| /usr/bin/awk -v p=\"$login\" '$2==p {print $1; exit}') | \n|  | print -r -- \"${sh:-$login}\" | \n|  | done | \n|  | } | \n|  |  | \n|  | foreground_job() {  # $1 = shell pid → \"pid argv\" of its foreground job, if any | \n|  | /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}' | \n|  | } | \n|  |  | \n|  | # Splits an agent's argv into the options a resume keeps (with their values), the first | \n|  | # positional word (a subcommand, or the start of a prompt or an old session id; positionals | \n|  | # are never kept, so a prompt is not sent again) and every option seen. | \n|  | typeset -ga KEPT; typeset -g FIRST='' SEEN=' ' | \n|  | parse_agent() {  # $1 = options that take a value, $2 = options to drop, rest = argv | \n|  | local takes=\" $1 \" drops=\" $2 \" a name; integer value; shift 2 | \n|  | KEPT=() FIRST='' SEEN=' ' | \n|  | while (( $# )); do | \n|  | a=$1; shift | \n|  | if [[ $a == -?* ]]; then | \n|  | name=${a%%=*}; SEEN+=\"$name \"; value=0 | \n|  | [[ $a != *=* && $takes == *\" $name \"* && $# -gt 0 && $1 != -* ]] && value=1 | \n|  | if [[ $drops == *\" $name \"* ]]; then | \n|  | (( value )) && shift | \n|  | else | \n|  | KEPT+=(\"$a\"); (( value )) && { KEPT+=(\"$1\"); shift } | \n|  | fi | \n|  | elif [[ -z $FIRST ]]; then | \n|  | FIRST=$a | \n|  | fi | \n|  | done | \n|  | } | \n|  | seen() { local f; for f; do [[ $SEEN == *\" $f \"* ]] && return 0; done; return 1 } | \n|  |  | \n|  | # omp keeps a registry per terminal: line 1 the session's cwd, line 2 its session file. | \n|  | resume_omp() {  # $1 = tty, $2 = cwd, rest = argv after `omp` → \"cwd<TAB>command\", or nothing | \n|  | local cwd=$2 reg=\"$OMP_DIR/terminal-sessions/$1\"; shift 2 | \n|  | parse_agent \"$OMP_TAKES\" \"$OMP_DROP\" \"$@\" | \n|  | seen -p --print --export --no-session --mode -h --help && return 0 | \n|  | [[ -r $reg ]] \\|\\| return 0 | \n|  | local sess_cwd=$(/usr/bin/sed -n 1p \"$reg\") | \n|  | local id=$(/usr/bin/sed -n 2p \"$reg\" \\| /usr/bin/sed -n 's#.*_$[0-9a-f-]\\{36\\}$\\.jsonl$#\\1#p') | \n|  | [[ -n $id ]] \\|\\| return 0 | \n|  | local -a cmd; cmd=(omp \"${(@q)KEPT}\" --resume \"$id\") | \n|  | print -r -- \"${sess_cwd:-$cwd}\"$'\\t'\"${(j: :)cmd}\" | \n|  | } | \n|  |  | \n|  | # Claude Code 2.1 keeps ~/.claude/sessions/<pid>.json with the live session id of each running | \n|  | # claude; it follows /clear and /resume. Older versions: the newest transcript in the folder | \n|  | # written since that claude started. | \n|  | resume_claude() {  # $1 = cwd, $2 = pid, rest = argv after `claude` → \"cwd<TAB>command\", or nothing | \n|  | local cwd=$1 pid=$2 state=\"$CLAUDE_DIR/sessions/$2.json\" id='' start; local -a t; shift 2 | \n|  | parse_agent \"$CLAUDE_TAKES\" \"$CLAUDE_DROP\" \"$@\" | \n|  | if [[ -r $state ]]; then | \n|  | local kind=$(/usr/bin/sed -n 's/.*\"kind\":\"$[^\"]*$\".*/\\1/p' \"$state\") | \n|  | [[ -z $kind \\|\\| $kind == interactive ]] \\|\\| return 0 | \n|  | id=$(/usr/bin/sed -n 's/.*\"sessionId\":\"$[0-9a-f-]\\{36\\}$\".*/\\1/p' \"$state\") | \n|  | [[ -n $id ]] && t=($CLAUDE_DIR/projects/*/$id.jsonl(N)) | \n|  | (( $#t )) \\|\\| id='' # nothing said in it yet: start fresh | \n|  | else | \n|  | seen -p --print --bg --background -h --help -v --version && return 0 | \n|  | [[ -n $FIRST && \" $CLAUDE_COMMANDS \" == *\" $FIRST \"* ]] && return 0 | \n|  | start=$(proc_start \"$pid\") | \n|  | t=($CLAUDE_DIR/projects/${${cwd:A}//[^A-Za-z0-9]/-}/*.jsonl(Nom[1])) | \n|  | (( $#t )) && (( $(/usr/bin/stat -f %m \"$t[1]\") >= ${start:-0} )) && id=${${t[1]:t}%.jsonl} | \n|  | fi | \n|  | local -a cmd; cmd=(claude \"${(@q)KEPT}\"); [[ -n $id ]] && cmd+=(--resume \"$id\") | \n|  | print -r -- \"$cwd\"$'\\t'\"${(j: :)cmd}\" | \n|  | } | \n|  |  | \n|  | # Codex writes each session to ~/.codex/sessions/.../rollout-<time>-<id>.jsonl and indexes | \n|  | # it in the threads table of ~/.codex/state_<n>.sqlite. | \n|  | codex_session() {  # $1 = codex pid, $2 = cwd → id of the session it is writing, if found | \n|  | local pids=$1 child id | \n|  | for child in $(/usr/bin/pgrep -P \"$1\"); do pids+=\",$child\"; done | \n|  | 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) | \n|  | if [[ -z $id ]]; then # not held open: the thread in this folder updated since this codex started | \n|  | local -a db; db=($CODEX_DIR/state_*.sqlite(Nom[1])) | \n|  | local start=$(proc_start \"$1\") a=${2//\"'\"/\"''\"} b=${${2:A}//\"'\"/\"''\"} | \n|  | (( $#db )) && [[ -n $start ]] && id=$(/usr/bin/sqlite3 -readonly \"$db[1]\" \\ | \n|  | \"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) | \n|  | fi | \n|  | print -r -- \"$id\" | \n|  | } | \n|  |  | \n|  | resume_codex() {  # $1 = cwd, $2 = pid, rest = argv after `codex` → \"cwd<TAB>command\", or nothing | \n|  | local cwd=$1 pid=$2; shift 2 | \n|  | parse_agent \"$CODEX_TAKES\" \"$CODEX_DROP\" \"$@\" | \n|  | seen -h --help -V --version && return 0 | \n|  | [[ -n $FIRST && \" $CODEX_COMMANDS \" == *\" $FIRST \"* ]] && return 0 | \n|  | local id=$(codex_session \"$pid\" \"$cwd\") | \n|  | local -a cmd; cmd=(codex); [[ -n $id ]] && cmd+=(resume \"$id\"); cmd+=(\"${(@q)KEPT}\") | \n|  | print -r -- \"$cwd\"$'\\t'\"${(j: :)cmd}\" | \n|  | } | \n|  |  | \n|  | # How to bring a pane back. An agent session becomes a resume command that runs by itself; | \n|  | # any other foreground command (ssh, vim, a dev server) is typed onto the prompt, not run. | \n|  | resumable_command() {  # $1 = tty, $2 = cwd, $3 = foreground pid, $4 = its argv → \"cwd<TAB>mode<TAB>command\" | \n|  | local tty=$1 cwd=$2 pid=$3 fg=$4 out='' | \n|  | [[ $fg == *ghostty-agent-restore* ]] && fg='' # never record this tool itself: `restart` would loop | \n|  | [[ -n $fg ]] \\|\\| { print -r -- \"$cwd\"$'\\t\\t'; return } | \n|  | local -a argv; argv=(${(s: :)fg})   # ps joins argv with spaces and keeps no quoting | \n|  | [[ ${argv[1]:t} == (node\\|bun) && ${${argv[2]:-}:t} == (omp\\|claude\\|codex) ]] && shift argv | \n|  | case ${argv[1]:t} in | \n|  | omp)    out=$(resume_omp \"$tty\" \"$cwd\" \"${(@)argv[2,-1]}\") ;; | \n|  | claude) out=$(resume_claude \"$cwd\" \"$pid\" \"${(@)argv[2,-1]}\") ;; | \n|  | codex)  out=$(resume_codex \"$cwd\" \"$pid\" \"${(@)argv[2,-1]}\") ;; | \n|  | esac | \n|  | if [[ -n $out ]]; then | \n|  | print -r -- \"${out%%$'\\t'*}\"$'\\trun\\t'\"${out#*$'\\t'}\" | \n|  | else | \n|  | print -r -- \"$cwd\"$'\\ttype\\t'\"$fg\" | \n|  | fi | \n|  | } | \n|  |  | \n|  | # The terminal id a pane's shell recorded at its first prompt; ignored once that shell is | \n|  | # gone, since a new pane can reuse the tty. | \n|  | pane_terminal() {  # $1 = tty, $2 = pane shell pid | \n|  | [[ -r $PANES/$1 ]] \\|\\| return 0 | \n|  | local -a f; f=(\"${(@ps:\\t:)$(<$PANES/$1)}\") | \n|  | [[ ${f[2]:-} == $2 ]] && print -r -- \"${f[1]}\" | \n|  | } | \n|  |  | \n|  | lock() { | \n|  | local i | \n|  | for i in {1..50}; do | \n|  | /bin/mkdir \"$LOCK\" 2>/dev/null && return 0 | \n|  | sleep 0.1 | \n|  | done | \n|  | return 1 | \n|  | } | \n|  | unlock() { /bin/rmdir \"$LOCK\" 2>/dev/null } | \n|  |  | \n|  | # True when current.tsv was taken under a Ghostty that is no longer running: the Mac | \n|  | # rebooted, or Ghostty quit, crashed or updated. | \n|  | from_previous_ghostty() {  # $1 = running Ghostty instance | \n|  | [[ -n $1 && -s $CURRENT && -r $INSTANCE && $(<$INSTANCE) != $1 ]] | \n|  | } | \n|  |  | \n|  | # Caller holds $LOCK. Arms the previous Ghostty's snapshot, once. | \n|  | arm_previous() {  # $1 = running Ghostty instance | \n|  | from_previous_ghostty \"$1\" \\|\\| return 0 | \n|  | /bin/cp -f \"$CURRENT\" \"$RESTORE\" && print -r -- \"$1\" > \"$INSTANCE\" | \n|  | } | \n|  |  | \n|  | cmd_snapshot() { | \n|  | local inst=$(ghostty_instance) | \n|  | [[ -n $inst ]] \\|\\| return 1   # Ghostty not running: keep the last snapshot | \n|  | local -A position; local i p | \n|  | [[ -r $LAYOUT ]] && while IFS=$'\\t' read -r i p; do position[$i]=$p; done < \"$LAYOUT\" | \n|  | local tmp=\"$CURRENT.tmp.$$\" sh tty id cwd job | \n|  | : > \"$tmp\" | \n|  | for sh in $(pane_shells); do | \n|  | tty=$(/bin/ps -o tty= -p \"$sh\" \\| /usr/bin/tr -d ' ') | \n|  | id=$(pane_terminal \"$tty\" \"$sh\"); p='' | \n|  | [[ -n $id ]] && p=${position[$id]:-} | \n|  | cwd=$(/usr/sbin/lsof -a -p \"$sh\" -d cwd -Fn 2>/dev/null \\| /usr/bin/sed -n 's/^n//p' \\| /usr/bin/head -1) | \n|  | job=$(foreground_job \"$sh\") | \n|  | print -r -- \"$tty\"$'\\t'\"$id\"$'\\t'\"$p\"$'\\t'\"$(resumable_command \"$tty\" \"${cwd:-$HOME}\" \"${job%% *}\" \"${job#* }\")\" >> \"$tmp\" | \n|  | done | \n|  | if [[ ! -s $tmp ]]; then | \n|  | /bin/rm -f \"$tmp\" # no panes (Ghostty starting or quitting): keep the last snapshot | \n|  | return 1 | \n|  | fi | \n|  | /usr/bin/sort -o \"$tmp\" \"$tmp\" | \n|  | lock \\|\\| { /bin/rm -f \"$tmp\"; return 1 } | \n|  | arm_previous \"$inst\" # the snapshot about to be replaced belongs to the previous Ghostty | \n|  | /bin/mv -f \"$tmp\" \"$CURRENT\" && print -r -- \"$inst\" > \"$INSTANCE\" | \n|  | unlock | \n|  | } | \n|  |  | \n|  | cmd_list() { | \n|  | local file=\"${1:-$CURRENT}\" | \n|  | [[ -r $file ]] \\|\\| { print \"no snapshot yet; run: ghostty-agent-restore snapshot\"; return 1 } | \n|  | print \"${file:t} · $(/bin/date -r \"$file\" '+%b %d %H:%M') · $(/usr/bin/wc -l < \"$file\" \\| /usr/bin/tr -d ' ') panes\" | \n|  | print \"  tty      window:tab  cwd                                      command\" | \n|  | /usr/bin/sort -t $'\\t' -k3,3 -k1,1 \"$file\" \\| /usr/bin/awk -F'\\t' '{ | \n|  | split($3, p, \".\"); where = ($3 == \"\" ? \"-\" : (p[1] + 0) \":\" (p[2] + 0) (p[3] + 0 > 1 ? \".\" (p[3] + 0) : \"\")) | \n|  | cwd = (index($4, ENVIRON[\"HOME\"]) == 1 ? \"~\" substr($4, length(ENVIRON[\"HOME\"]) + 1) : $4) | \n|  | printf \"  %-8s %-11s %-40s %s%s\\n\", $1, where, cwd, ($6 == \"\" ? \"(shell)\" : $6), ($5 == \"type\" ? \"   (typed, not run)\" : \"\")}' | \n|  | } | \n|  |  | \n|  | cmd_arm() { | \n|  | [[ -s $CURRENT ]] \\|\\| { print \"nothing to arm\"; return 1 } | \n|  | /bin/rm -rf \"$LOCK\" | \n|  | /bin/cp -f \"$CURRENT\" \"$RESTORE\" | \n|  | 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):\" | \n|  | cmd_list \"$RESTORE\" | \n|  | } | \n|  |  | \n|  | # Ghostty answers AppleScript with each terminal's id, position and title, so a shell finds | \n|  | # its own terminal id by setting a unique title and looking for it. Sets TERMINAL_ID and | \n|  | # LIVE_IDS, saves every terminal's position to $LAYOUT, and records this pane for snapshots. | \n|  | TERMINAL_ID='' LIVE_IDS=' ' | \n|  | identify() {  # $1 = tty, $2 = pane shell pid | \n|  | local token=\"ghostty-agent-restore-$$-$RANDOM\" out='' i | \n|  | print -n -- $'\\e]2;'\"$token\"$'\\a' > /dev/tty | \n|  | for i in {1..10}; do | \n|  | # Two bulk reads (one Apple event each) instead of one event per property per terminal: | \n|  | # about 0.1 s instead of 0.5 s with a dozen terminals. Ids are read again afterwards; | \n|  | # if a tab opened or closed in between, the rows could pair up wrong, so retry. | \n|  | out=$(/usr/bin/osascript -e 'with timeout of 3 seconds | \n|  | tell application \"Ghostty\" | \n|  | set ids to id of terminals of tabs of windows | \n|  | set names to name of terminals of tabs of windows | \n|  | set again to id of terminals of tabs of windows | \n|  | end tell | \n|  | end timeout | \n|  | if again is not equal to ids then return \"\" | \n|  | set out to \"\" | \n|  | repeat with wi from 1 to count of ids | \n|  | repeat with ti from 1 to count of item wi of ids | \n|  | set tabIds to item ti of item wi of ids | \n|  | set tabNames to item ti of item wi of names | \n|  | repeat with ki from 1 to count of tabIds | \n|  | set out to out & (item ki of tabIds) & (character id 9) & wi & \".\" & ti & \".\" & ki & (character id 9) & (item ki of tabNames) & linefeed | \n|  | end repeat | \n|  | end repeat | \n|  | end repeat | \n|  | return out' 2>/dev/null) \\|\\| break | \n|  | TERMINAL_ID=${${(M)${(f)out}:#*$'\\t'$token}%%$'\\t'*} | \n|  | [[ -n $TERMINAL_ID ]] && break | \n|  | sleep 0.05 | \n|  | done | \n|  | print -n -- $'\\e]2;'\"${PWD/#$HOME/~}\"$'\\a' > /dev/tty | \n|  | [[ -n $out ]] \\|\\| return 0 | \n|  | local -a ids; ids=(${${(f)out}%%$'\\t'*})   # unquoted: inside \"...\" the lines would be joined first | \n|  | LIVE_IDS=\" ${(j: :)ids} \" | \n|  | 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.$$\" \\ | \n|  | && /bin/mv -f \"$LAYOUT.tmp.$$\" \"$LAYOUT\" | \n|  | [[ -n $TERMINAL_ID ]] && print -r -- \"$TERMINAL_ID\"$'\\t'\"$2\" > \"$PANES/$1\" | \n|  | } | \n|  |  | \n|  | # Take this pane's entry from restore.tsv. A pane without one (Ghostty did not restore the | \n|  | # windows, so this is a new tab) takes the session that came first in the old tab order | \n|  | # among those whose terminal is gone. Prints the entry. | \n|  | take_entry() { | \n|  | lock \\|\\| return | \n|  | local n=$(/usr/bin/awk -F'\\t' -v me=\"$TERMINAL_ID\" -v live=\"$LIVE_IDS\" ' | \n|  | me != \"\" && $2 == me { print NR; mine = 1; exit } | \n|  | $6 != \"\" && ($2 == \"\" \\|\\| index(live, \" \" $2 \" \") == 0) { | \n|  | key = ($3 == \"\" ? \"999\" : $3) \"\\t\" $1 | \n|  | if (spare == \"\" \\|\\| key < best) { spare = NR; best = key } | \n|  | } | \n|  | END { if (!mine && spare != \"\") print spare }' \"$RESTORE\" 2>/dev/null) | \n|  | local line='' | \n|  | if [[ -n $n ]]; then | \n|  | line=$(/usr/bin/sed -n \"${n}p\" \"$RESTORE\") | \n|  | /usr/bin/sed \"${n}d\" \"$RESTORE\" > \"$RESTORE.tmp\" && /bin/mv -f \"$RESTORE.tmp\" \"$RESTORE\" | \n|  | [[ -s $RESTORE ]] \\|\\| /bin/rm -f \"$RESTORE\" | \n|  | fi | \n|  | unlock | \n|  | print -r -- \"$line\" | \n|  | } | \n|  |  | \n|  | # Run by the .zshrc hook at a pane's first prompt, with the pane shell's pid. Learns this | \n|  | # pane's terminal id, arms the previous Ghostty's snapshot if this is a new Ghostty, and | \n|  | # prints \"<mode><TAB><command line>\" for this pane, if it gets an entry. | \n|  | cmd_claim() { | \n|  | local shell=${1:-$PPID} | \n|  | local login=$(/bin/ps -o ppid= -p \"$shell\" \\| /usr/bin/tr -d ' ') | \n|  | [[ $(/bin/ps -o comm= -p \"$login\") == */login ]] \\|\\| return 0   # a pane's own shell, not a nested one | \n|  | identify \"$(/bin/ps -o tty= -p \"$shell\" \\| /usr/bin/tr -d ' ')\" \"$shell\" | \n|  | local inst=$(ghostty_instance) | \n|  | if from_previous_ghostty \"$inst\" && lock; then | \n|  | arm_previous \"$inst\" | \n|  | unlock | \n|  | fi | \n|  | [[ -r $RESTORE ]] \\|\\| return 0 | \n|  | local age=$(( $(/bin/date +%s) - $(/usr/bin/stat -f %m \"$RESTORE\") )) | \n|  | if (( age > FRESH_SECONDS )); then | \n|  | /bin/rm -f \"$RESTORE\"; return 0 | \n|  | fi | \n|  | local line=$(take_entry) | \n|  | local -a f; f=(\"${(@ps:\\t:)line}\") | \n|  | local cwd=${f[4]:-} mode=${f[5]:-} command=${line#*$'\\t'*$'\\t'*$'\\t'*$'\\t'*$'\\t'} | \n|  | [[ -n $line && -n $mode && -n $command ]] \\|\\| return 0 | \n|  | # real path, so a directory since moved but still reached through a symlink works | \n|  | [[ -d $cwd && ${cwd:A} != ${PWD:A} ]] && command=\"cd -- ${(q)cwd:A} && $command\" | \n|  | print -r -- \"$mode\"$'\\t'\"$command\" | \n|  | } | \n|  |  | \n|  | # zsh code for .zshrc. The claim runs in precmd, where p10k's instant prompt has stdin on | \n|  | # /dev/null and stdout on a file, so nothing is run there: the command is put on the first | \n|  | # prompt's command line, and submitted only for an agent session. | \n|  | cmd_hook() { | \n|  | /bin/cat <<'EOF' | \n|  | _ghostty_agent_restore_claim() { | \n|  | precmd_functions=(${precmd_functions:#_ghostty_agent_restore_claim}) | \n|  | local out=$(ghostty-agent-restore claim $$) | \n|  | [[ -n $out ]] \\|\\| return 0 | \n|  | typeset -g _ghostty_agent_restore_mode=${out%%$'\\t'*} _ghostty_agent_restore_cmd=${out#*$'\\t'} | \n|  | zle -N _ghostty_agent_restore_run | \n|  | autoload -Uz add-zle-hook-widget && add-zle-hook-widget line-init _ghostty_agent_restore_run | \n|  | } | \n|  | _ghostty_agent_restore_run() { | \n|  | [[ -n ${_ghostty_agent_restore_cmd:-} ]] \\|\\| return 0 | \n|  | BUFFER=$_ghostty_agent_restore_cmd | \n|  | CURSOR=${#BUFFER} | \n|  | unset _ghostty_agent_restore_cmd | \n|  | [[ $_ghostty_agent_restore_mode == run ]] && zle accept-line | \n|  | } | \n|  | precmd_functions+=(_ghostty_agent_restore_claim) | \n|  | EOF | \n|  | } | \n|  |  | \n|  | cmd_restart() { | \n|  | cmd_snapshot \\|\\| { print \"Ghostty is not running\"; return 1 } | \n|  | cmd_list | \n|  | print \"\\nQuitting Ghostty (confirm the dialog if it asks). It relaunches and every pane resumes.\" | \n|  | nohup /bin/zsh -c ' | \n|  | /usr/bin/osascript -e '\"'\"'tell application \"Ghostty\" to quit'\"'\"' >/dev/null 2>&1 | \n|  | for i in {1..120}; do /bin/ps -axo comm= \\| /usr/bin/grep -qx \"'\"$GHOSTTY_BIN\"'\" \\|\\| break; sleep 0.5; done | \n|  | sleep 1 | \n|  | /usr/bin/open -a Ghostty | \n|  | ' >/dev/null 2>&1 &! | \n|  | } | \n|  |  | \n|  | cmd_install() { | \n|  | local plist=\"$HOME/Library/LaunchAgents/$LABEL.plist\" | \n|  | /bin/mkdir -p \"$HOME/Library/LaunchAgents\" | \n|  | /bin/cat > \"$plist\" <<EOF | \n|  | <?xml version=\"1.0\" encoding=\"UTF-8\"?> | \n|  | <!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\"> | \n|  | <plist version=\"1.0\"><dict> | \n|  | <key>Label</key><string>$LABEL</string> | \n|  | <key>ProgramArguments</key><array><string>/bin/zsh</string><string>$HOME/.local/bin/ghostty-agent-restore</string><string>snapshot</string></array> | \n|  | <key>StartInterval</key><integer>120</integer> | \n|  | <key>RunAtLoad</key><true/> | \n|  | <key>StandardErrorPath</key><string>$DIR/launchd.log</string> | \n|  | </dict></plist> | \n|  | EOF | \n|  | /bin/launchctl bootout \"gui/$UID/$LABEL\" 2>/dev/null | \n|  | /bin/launchctl bootstrap \"gui/$UID\" \"$plist\" && print \"launchd: snapshot every 2 min ($LABEL)\" | \n|  | local hook='command -v ghostty-agent-restore >/dev/null && [[ -n ${GHOSTTY_RESOURCES_DIR:-} ]] && eval \"$(ghostty-agent-restore hook)\"  # ghostty-agent-restore' | \n|  | local rc=\"$HOME/.zshrc\" | \n|  | if /usr/bin/grep -q '# ghostty-agent-restore$' \"$rc\" 2>/dev/null; then | \n|  | /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.$$\" | \n|  | print \".zshrc: hook updated\" | \n|  | else | \n|  | print -- \"\\n# Resume what each Ghostty pane was running before Ghostty or the Mac restarted\\n$hook\" >> \"$rc\" | \n|  | print \".zshrc: hook added\" | \n|  | fi | \n|  | } | \n|  |  | \n|  | case \"${1:-}\" in | \n|  | snapshot) cmd_snapshot ;; | \n|  | list)     cmd_list \"${2:-}\" ;; | \n|  | restart)  cmd_restart ;; | \n|  | arm)      cmd_arm ;; | \n|  | claim)    cmd_claim \"${2:-}\" ;; | \n|  | hook)     cmd_hook ;; | \n|  | install)  cmd_install ;; | \n|  | *) /usr/bin/sed -n '2,17p' \"$0\"; exit 64 ;; | \n|  | esac |", "url": "https://wpnews.pro/news/ghostty-agent-restore-after-a-reboot-or-ghostty-restart-every-ghostty-tab-the-or", "canonical_source": "https://gist.github.com/rovirmani/780fcb09a14eeddbef2f0fadd3501310", "published_at": "2026-10-17 07:38:44+00:00", "updated_at": "2026-10-19 00:20:14.777375+00:00", "lang": "en", "topics": ["ai-tools", "developer-tools", "ai-agents"], "entities": ["Ghostty", "Claude Code", "Codex", "omp", "macOS", "zsh"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/ghostty-agent-restore-after-a-reboot-or-ghostty-restart-every-ghostty-tab-the-or", "markdown": "https://wpnews.pro/news/ghostty-agent-restore-after-a-reboot-or-ghostty-restart-every-ghostty-tab-the-or.md", "text": "https://wpnews.pro/news/ghostty-agent-restore-after-a-reboot-or-ghostty-restart-every-ghostty-tab-the-or.txt", "jsonld": "https://wpnews.pro/news/ghostty-agent-restore-after-a-reboot-or-ghostty-restart-every-ghostty-tab-the-or.jsonld"}}