Use whenever you are about to restart a service your current session runs inside (Telegram bridge, gateway, broker tunnel), e.g. config reload, timeout tuning, self-update checkout swap. Detaches restart+verify into a systemd-run unit first so verification survives. Not for peer/unrelated services.
Install
npx skillscat add jinwon-int/ccc-node/bridge-detached-self-restart Install via the SkillsCat registry.
bridge-detached-self-restart
The trap
If your current shell/agent session is a child of (or otherwise depends on)
the very systemd service you are restarting, systemctl restart <svc> does
not return cleanly to a script that then verifies the result — the restart
tears down your own process tree partway through, so any verification lines
written after the restart command in the same script/turn silently never
execute. The operator sees "restarted" with no confirmation that the new
process actually came up healthy with the new config. This is a narrower,
more dangerous case than bridge-safe-detached-run (which is about the
session dying under a long job) — here the action itself is what kills
the caller.
When to Use
- Restarting the Telegram/CCC bridge service from a bridge session (e.g. after
editing.envtimeout/config values — seebridge-timeout-performance-tuning-and-verification) - Restarting a gateway/broker-tunnel unit from a session that runs on top of it
- Any self-update flow that swaps the checkout and then restarts the service
serving the current session (see/root/.ccc-node/self-restart-verify.sh
for a prior art example of this exact pattern) - Not for restarting an unrelated peer service you merely operate — a plain
systemctl restart <svc>+ inline check is fine when your own session
doesn't run inside it.
Procedure
Write the restart+verify sequence as a single detached script, not as
inline commands. It must itself write its output to a persistent log path
(not stdout) because stdout is going to the session that's about to die:cat > /tmp/self-restart-verify.sh <<'EOF' #!/usr/bin/env bash set -uo pipefail OUT=$HOME/.claude/state/self-restart-verify-<job>.log { echo "=== restart $(date -u +%FT%TZ) ==="; } > "$OUT" 2>&1 systemctl restart <own-service>.service >> "$OUT" 2>&1 sleep 15 { systemctl show <own-service> -p ActiveState,SubState,MainPID,NRestarts # + any app-specific health check (health.json, curl localhost, etc.) echo "=== done $(date -u +%FT%TZ) ===" } >> "$OUT" 2>&1 EOF chmod +x /tmp/self-restart-verify.shLaunch it as a
systemd-runtransient unit with--collect, followingbridge-safe-detached-runStep 1 — injectHOME/PATHexplicitly, since
systemd-run does not inherit the shell environment:systemd-run --unit self-restart-<job> --collect \ --property=Environment=HOME=$HOME \ --property=Environment=PATH=$HOME/.local/bin:/usr/local/bin:/usr/bin:/bin \ /tmp/self-restart-verify.shThis is the critical step: because the unit is owned by PID 1 (not by the
session/service you're about to kill), it survives the restart it triggers.Register the job so the next session (which will be a fresh process
spawned by the restarted service, not a continuation of this one) can find
the evidence:python3 ~/.claude/hooks/lib/detached_jobs.py register \ --unit self-restart-<job> --log "$OUT" --summary "self-restart <own-service> for <reason>"End the turn / do not attempt to verify inline. The current session is
about to die when the restart fires; there is nothing to poll from inside
it. Trust the registry — SessionStart on the next session sweeps it and
surfacesdone/running/lostwith the real exit evidence.On the next session, read the log, confirm
MainPIDchanged and the
app-specific health check passed, thenackthe job:tail -30 "$OUT" python3 ~/.claude/hooks/lib/detached_jobs.py ack --unit self-restart-<job>
Safety
- Restarting bridge/gateway/broker-tunnel infrastructure is approval-gated
(broker-public-tunnel and auth-proxy units on broker/relay hosts — e.g.<node>-broker-public-tunnel.service— must not be stopped without explicit
operator approval; broker/Gateway restarts require fresh approval per
USER.md). Detaching the restart does not bypass that — get approval before
step 2, not after. - Never put secrets in the unit name/command line (visible in
systemctl/journal). - If the service does not come back (
ActiveStatenotactiveafter the
sleep window, or the app health check fails), the log is the only forensic
trail — do not assume success just because the registry shows the job asdone; read the actual verification output.
Verification
self-restart-<job>.serviceshows insystemd-run's immediate return
("Running as unit: ...") before the restart fires.- The log file (not stdout — stdout died with the session) contains both a
newMainPIDand a passing app-specific health check. detached_jobs.py liston the next session shows the job asdonewith
real exit evidence, notlost.- Compare pre- and post-restart
MainPID/config (e.g.cat /proc/<pid>/environ)
to confirm the intended change actually took effect, perbridge-timeout-performance-tuning-and-verification.