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/devbox-health.tmpl
files/remote-setup.sh files/claude-notify.tmpl

.PHONY: check residue preflight provision setup sync all destroy

Expand Down
7 changes: 2 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ manage, wired for Claude Code — and rebuild the whole thing from this repo in

- **Tailnet-only.** Zero public TCP ports. Access is Tailscale SSH: your tailnet identity is the credential.
- **Hardened and self-maintaining.** UFW default-deny, key-only sshd, automatic security patches with a nightly reboot window.
- **Self-alerting.** Pushes to your phone when the disk fills or a service fails.
- **Ready for development.** fish + starship, persistent tmux, Node/pnpm, Docker, Claude Code with configurable skills and phone notifications (and OpenAI Codex CLI, optional).
- **Instant previews.** `http://devbox:<port>` reaches any dev server or container on the box — even one bound to localhost.

Expand Down Expand Up @@ -40,8 +39,7 @@ flowchart LR
| Sessions | tmux auto-attach on SSH + resurrect/continuum | Survives disconnects *and* the 04:00 patch reboots |
| Localhost preview | iptables(-nft) REDIRECT → `tailnet-devproxy.py` (SO_ORIGINAL_DST) | `http://devbox:<port>` works even for servers bound to `127.0.0.1`/`::1` |
| Containers | Docker + Compose, publishes default to `127.0.0.1` | Containers stay off the internet (Docker bypasses UFW — see FOOTGUNS); the tailnet reaches them via the devproxy |
| Self-alerting | hourly root timer → Pushover | Pushes only on trouble: disk ≥85% or a failed unit, repeating hourly until fixed |
| Notifications | Claude Code hooks → Pushover | Presence-aware; includes turn-failure alerts |
| Notifications | Claude Code hooks → Pushover | Presence-aware; pushes only when Claude is waiting on you or a turn failed |
| Recovery | Hetzner rescue mode / console | No credentials live on the box; reset root via Hetzner if ever needed |

## Prerequisites
Expand Down Expand Up @@ -99,8 +97,7 @@ 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. A **"devbox health"** push is the hourly monitor flagging low
disk or a failed unit.
quiet while you're active in tmux.

## Rebuild and teardown

Expand Down
36 changes: 0 additions & 36 deletions files/devbox-health.tmpl

This file was deleted.

24 changes: 0 additions & 24 deletions files/remote-setup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -66,30 +66,6 @@ if [ "$(cat "$S/install-codex" 2>/dev/null)" = 1 ]; then
|| echo "WARN: bubblewrap install failed (codex falls back to a bundled copy)"
fi

echo "== health-check timer (root systemd, hourly) =="
sudo install -m 700 -o root -g root "$S/devbox-health" /usr/local/bin/devbox-health
sudo tee /etc/systemd/system/devbox-health.service >/dev/null <<'UNIT'
[Unit]
Description=Devbox health check -> Pushover
Wants=network-online.target
After=network-online.target
[Service]
Type=oneshot
ExecStart=/usr/local/bin/devbox-health
UNIT
sudo tee /etc/systemd/system/devbox-health.timer >/dev/null <<'UNIT'
[Unit]
Description=Hourly devbox health check
[Timer]
OnCalendar=hourly
RandomizedDelaySec=300
Persistent=true
[Install]
WantedBy=timers.target
UNIT
sudo systemctl daemon-reload
sudo systemctl enable --now devbox-health.timer >/dev/null

echo "== git identity =="
# Identity arrives as files (see setup-user.sh) so no quoting layer ever parses it.
GIT_NAME=$(cat "$S/git-name" 2>/dev/null || true)
Expand Down
7 changes: 2 additions & 5 deletions setup-user.sh
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ source ./secrets.env
DEVBOX_NAME="${DEVBOX_NAME:-devbox}"
DEV_USER="${DEV_USER:-dev}"

# Pushover keys render into a ROOT-executed script (devbox-health); real keys
# are alphanumeric, so enforce exactly that — kills the injection class.
# Pushover keys render (via sed) into the claude-notify hook; real keys are
# alphanumeric, so enforce exactly that — keeps the render clean and safe.
for v in PUSHOVER_TOKEN PUSHOVER_USER; do
val="${!v:-}"
if [ -n "$val" ] && ! echo "$val" | grep -Eq '^[A-Za-z0-9]+$'; then
Expand All @@ -37,9 +37,6 @@ 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/devbox-health.tmpl > "$staging/devbox-health"
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