Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
SCRIPTS = preflight.sh provision.sh setup-user.sh sync-code.sh destroy.sh \
files/remote-setup.sh files/claude-notify.tmpl
files/remote-setup.sh files/claude-notify.tmpl files/codex-notify.tmpl

.PHONY: check residue preflight provision setup sync all destroy

Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,8 @@ Optional: enable **Tailscale Serve** on your tailnet for HTTPS preview URLs —
- **`claude` in any repo** pushes to your phone only when it's genuinely waiting on you — idle
after handing control back, or blocked on a decision — plus turn failures. It deliberately does
*not* ping on every completed turn (that floods during autonomous multi-step work), and stays
quiet while you're active in tmux.
quiet while you're active in tmux. Codex (when `INSTALL_CODEX=1`) pushes the same way when it
hands a turn back — it exposes only a turn-complete event, but that's its genuine "over to you" moment.

## Rebuild and teardown

Expand Down
48 changes: 48 additions & 0 deletions files/codex-notify.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
#!/usr/bin/env bash
# OpenAI Codex CLI notify program -> Pushover push notifications.
# Wired via `notify = ["<this script>"]` in ~/.codex/config.toml. Codex passes
# the event JSON as the FINAL ARGV argument (not stdin), and currently emits
# only "agent-turn-complete" — there is no idle/error/needs-input event to
# filter on (unlike Claude Code's hooks), so this pushes when Codex hands a
# turn back to you. That's the genuine "over to you" moment; Codex does not
# flood with intermediate stops. Presence-suppressed while you're active in the
# tmux session, exactly like the Claude hook.
# Rendered from codex-notify.tmpl by setup-user.sh (placeholders from secrets.env).
PO_TOKEN="__PUSHOVER_TOKEN__"
PO_USER="__PUSHOVER_USER__"
PRESENCE_WINDOW=60

# Codex hands the payload as the last argument; fall back to stdin if a future
# version ever switches, so we don't go silently dead.
payload="${1:-}"
[ -z "$payload" ] && payload=$(cat 2>/dev/null)
[ -z "$payload" ] && exit 0

type=$(jq -r '.type // empty' <<<"$payload" 2>/dev/null)
[ "$type" = "agent-turn-complete" ] || exit 0

# Codex's payload carries the project dir and the message it ended on.
proj=$(jq -r '.cwd // empty' <<<"$payload" 2>/dev/null); proj=$(basename "${proj:-$PWD}")
body=$(jq -r '."last-assistant-message" // empty' <<<"$payload" 2>/dev/null | tr '\n\t' ' ' | sed 's/ */ /g' | head -c 280)
body=${body:-"Turn complete — over to you"}

# Presence check: if a tmux client on this session saw activity in the last
# $PRESENCE_WINDOW seconds, you're at the terminal — stay quiet.
if [ -n "$TMUX" ] && command -v tmux >/dev/null 2>&1; then
sess=$(tmux display-message -p '#{session_name}' 2>/dev/null)
last=$(tmux list-clients -t "$sess" -F '#{client_activity}' 2>/dev/null | sort -rn | head -1)
if [ -n "$last" ] && [ $(( $(date +%s) - last )) -lt "$PRESENCE_WINDOW" ]; then
exit 0
fi
fi

# The split literal keeps the render sed from rewriting this guard (see the
# Claude hook for the full explanation).
if [ -n "$PO_TOKEN" ] && [ "$PO_TOKEN" != "__PUSHOVER""_TOKEN__" ]; then
curl -s -o /dev/null --max-time 5 \
--form-string "token=$PO_TOKEN" --form-string "user=$PO_USER" \
--form-string "title=Codex · $proj" --form-string "message=$body" \
--form-string "priority=1" --form-string "sound=intermission" \
https://api.pushover.net/1/messages.json || true
fi
exit 0
12 changes: 12 additions & 0 deletions files/remote-setup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,18 @@ if [ "$(cat "$S/install-codex" 2>/dev/null)" = 1 ]; then
command -v bwrap >/dev/null \
|| sudo DEBIAN_FRONTEND=noninteractive apt-get -yq install bubblewrap >/dev/null 2>&1 \
|| echo "WARN: bubblewrap install failed (codex falls back to a bundled copy)"
# Phone notifications (same Pushover pipe as Claude): Codex calls a `notify`
# program on agent-turn-complete. Wire it via config.toml's root `notify` key,
# which per TOML must precede any [tables] — so prepend it; idempotent.
install -m 700 "$S/codex-notify" ~/.local/bin/codex-notify
mkdir -p ~/.codex
if ! grep -q '^notify' ~/.codex/config.toml 2>/dev/null; then
if [ -f ~/.codex/config.toml ]; then
sed -i "1i notify = [\"$HOME/.local/bin/codex-notify\"]" ~/.codex/config.toml
else
echo "notify = [\"$HOME/.local/bin/codex-notify\"]" > ~/.codex/config.toml
fi
fi
fi

echo "== git identity =="
Expand Down
3 changes: 3 additions & 0 deletions setup-user.sh
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ cp files/config.fish files/tmux.conf files/fnm.fish files/remote-setup.sh "$stag
sed -e "s|__PUSHOVER_TOKEN__|${PUSHOVER_TOKEN:-}|g" \
-e "s|__PUSHOVER_USER__|${PUSHOVER_USER:-}|g" \
files/claude-notify.tmpl > "$staging/claude-notify"
sed -e "s|__PUSHOVER_TOKEN__|${PUSHOVER_TOKEN:-}|g" \
-e "s|__PUSHOVER_USER__|${PUSHOVER_USER:-}|g" \
files/codex-notify.tmpl > "$staging/codex-notify"
sed -e "s|__DEVBOX_NAME__|$DEVBOX_NAME|g" files/vite-hosts.fish > "$staging/vite-hosts.fish"
sed -e "s|__DEV_USER__|$DEV_USER|g" files/claude-settings.json > "$staging/claude-settings.json"
printf '%s' "${GIT_NAME:-}" > "$staging/git-name"
Expand Down
Loading