← back to Ticket Guard
harden ticket-guard after second-model review: pid+start identity, no-tty guard, throttle (TK-11621)
1d2b3131f6696ea9f7dc73957116a8f6063f5b16 · 2026-09-13 16:41:45 -0700 · Steve
Codex was spend-capped (TK-11571 condition); review came from Kimi k3. Its sharpest
point matched my own adversarial pass: pid ALONE is not a process identity because
macOS recycles pids, so a dead writer's pid can be re-issued and re-validate a stale
record. Now requires pid AND start time (ps -o lstart= is byte-identical to the
record's owner.started, verified on ttys003).
Also from that review: a session with no controlling tty (headless claude -p) would
have probed a degenerate state path and let ttyless sessions inherit each other's
binding -> now an explicit fail-loud guard.
Added a 15-min per-identity throttle: without it a genuinely ticketless session gets
the reminder on every prompt forever, burning context to repeat itself. Stamp is keyed
to pid+start so a NEW session is never silenced by a dead one's stamp.
Perf: start time resolved LAZILY, only on the path that needs it — the 45 of 77
launcher-bound sessions exit at step 2 and never pay for it. 0.48s -> 0.03s.
Negative test 8 -> 14 cases, all pass, incl. pid-reuse, no-tty, missing start time,
and all three throttle behaviours.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DkVx1x2ppxm6tdEXngBj2P
Files touched
M test-ticket-guard.shM ticket-guard.sh
Diff
commit 1d2b3131f6696ea9f7dc73957116a8f6063f5b16
Author: Steve <steve@designerwallcoverings.com>
Date: Sun Sep 13 16:41:45 2026 -0700
harden ticket-guard after second-model review: pid+start identity, no-tty guard, throttle (TK-11621)
Codex was spend-capped (TK-11571 condition); review came from Kimi k3. Its sharpest
point matched my own adversarial pass: pid ALONE is not a process identity because
macOS recycles pids, so a dead writer's pid can be re-issued and re-validate a stale
record. Now requires pid AND start time (ps -o lstart= is byte-identical to the
record's owner.started, verified on ttys003).
Also from that review: a session with no controlling tty (headless claude -p) would
have probed a degenerate state path and let ttyless sessions inherit each other's
binding -> now an explicit fail-loud guard.
Added a 15-min per-identity throttle: without it a genuinely ticketless session gets
the reminder on every prompt forever, burning context to repeat itself. Stamp is keyed
to pid+start so a NEW session is never silenced by a dead one's stamp.
Perf: start time resolved LAZILY, only on the path that needs it — the 45 of 77
launcher-bound sessions exit at step 2 and never pay for it. 0.48s -> 0.03s.
Negative test 8 -> 14 cases, all pass, incl. pid-reuse, no-tty, missing start time,
and all three throttle behaviours.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DkVx1x2ppxm6tdEXngBj2P
---
test-ticket-guard.sh | 55 ++++++++++++++++++++++-----
ticket-guard.sh | 103 +++++++++++++++++++++++++++++++++++++++------------
2 files changed, 125 insertions(+), 33 deletions(-)
diff --git a/test-ticket-guard.sh b/test-ticket-guard.sh
index 83c2dbc..a1a624c 100644
--- a/test-ticket-guard.sh
+++ b/test-ticket-guard.sh
@@ -19,18 +19,18 @@ ck(){ # ck <name> <expect: NUDGE|SILENT> <output>
mk(){ printf '%s' "$2" > "$TMP/$1.json"; }
# 1. BOUND by launcher argv (a run-ticket.sh session) -> must stay SILENT
-out="$(TICKET_GUARD_STATE_DIR=$TMP TICKET_GUARD_FAKE_PID=111 TICKET_GUARD_FAKE_TTY=ttysA \
+out="$(TICKET_GUARD_THROTTLE_MIN=0 TICKET_GUARD_STATE_DIR=$TMP TICKET_GUARD_FAKE_PID=111 TICKET_GUARD_FAKE_TTY=ttysA \
TICKET_GUARD_FAKE_ARGV='claude --model opus export TK_AGENT=claude-run-11621. You are driving ticket TK-11621' bash "$HOOK")"
ck "bound via launcher argv" SILENT "$out"
# 2. INJECTED FAULT: unbound session, no record at all -> must go RED
-out="$(TICKET_GUARD_STATE_DIR=$TMP TICKET_GUARD_FAKE_PID=222 TICKET_GUARD_FAKE_TTY=ttysMISSING \
+out="$(TICKET_GUARD_THROTTLE_MIN=0 TICKET_GUARD_STATE_DIR=$TMP TICKET_GUARD_FAKE_PID=222 TICKET_GUARD_FAKE_TTY=ttysMISSING \
TICKET_GUARD_FAKE_ARGV='claude' bash "$HOOK")"
ck "FAULT: bare claude, no status record" NUDGE "$out"
# 3. BOUND by an identity-MATCHED record -> SILENT
-mk ttysB '{"owner":{"pid":333,"tty":"ttysB"},"ticket":"TK-9999"}'
-out="$(TICKET_GUARD_STATE_DIR=$TMP TICKET_GUARD_FAKE_PID=333 TICKET_GUARD_FAKE_TTY=ttysB \
+mk ttysB '{"owner":{"pid":333,"tty":"ttysB","started":"Sun Sep 13 10:00:00 2026"},"ticket":"TK-9999"}'
+out="$(TICKET_GUARD_THROTTLE_MIN=0 TICKET_GUARD_STATE_DIR=$TMP TICKET_GUARD_FAKE_PID=333 TICKET_GUARD_FAKE_TTY=ttysB TICKET_GUARD_FAKE_STARTED="Sun Sep 13 10:00:00 2026" \
TICKET_GUARD_FAKE_ARGV='claude --continue' bash "$HOOK")"
ck "bound via own identity-matched record" SILENT "$out"
@@ -40,33 +40,68 @@ ck "bound via own identity-matched record" SILENT "$out"
# (Real instance observed on ttys073: record named pid 21464 @11:29 while the live
# session was pid 84660 @16:08.)
mk ttysC '{"owner":{"pid":21464,"tty":"ttysC"},"ticket":"TK-11155"}'
-out="$(TICKET_GUARD_STATE_DIR=$TMP TICKET_GUARD_FAKE_PID=84660 TICKET_GUARD_FAKE_TTY=ttysC \
+out="$(TICKET_GUARD_THROTTLE_MIN=0 TICKET_GUARD_STATE_DIR=$TMP TICKET_GUARD_FAKE_PID=84660 TICKET_GUARD_FAKE_TTY=ttysC \
TICKET_GUARD_FAKE_ARGV='claude' bash "$HOOK")"
ck "FAULT: stale-slot record from a previous session" NUDGE "$out"
# 5. INJECTED FAULT: record present, identity matches, but no ticket -> RED
-mk ttysD '{"owner":{"pid":444,"tty":"ttysD"},"ticket":"","ticket_source":"unbound"}'
-out="$(TICKET_GUARD_STATE_DIR=$TMP TICKET_GUARD_FAKE_PID=444 TICKET_GUARD_FAKE_TTY=ttysD \
+mk ttysD '{"owner":{"pid":444,"tty":"ttysD","started":"Sun Sep 13 10:00:00 2026"},"ticket":"","ticket_source":"unbound"}'
+out="$(TICKET_GUARD_THROTTLE_MIN=0 TICKET_GUARD_STATE_DIR=$TMP TICKET_GUARD_FAKE_PID=444 TICKET_GUARD_FAKE_TTY=ttysD TICKET_GUARD_FAKE_STARTED="Sun Sep 13 10:00:00 2026" \
TICKET_GUARD_FAKE_ARGV='claude' bash "$HOOK")"
ck "FAULT: matched record but ticket empty" NUDGE "$out"
# 6. INJECTED FAULT: corrupt/unreadable record -> UNMEASURED -> RED, never silent
printf '%s' '{not json' > "$TMP/ttysE.json"
-out="$(TICKET_GUARD_STATE_DIR=$TMP TICKET_GUARD_FAKE_PID=555 TICKET_GUARD_FAKE_TTY=ttysE \
+out="$(TICKET_GUARD_THROTTLE_MIN=0 TICKET_GUARD_STATE_DIR=$TMP TICKET_GUARD_FAKE_PID=555 TICKET_GUARD_FAKE_TTY=ttysE \
TICKET_GUARD_FAKE_ARGV='claude' bash "$HOOK")"
ck "FAULT: corrupt record (unmeasured must not pass)" NUDGE "$out"
# 7. A near-miss argv must NOT be accepted as a binding (guards the regex itself):
# a session that merely MENTIONS a ticket in prose is not bound by the launcher.
-out="$(TICKET_GUARD_STATE_DIR=$TMP TICKET_GUARD_FAKE_PID=777 TICKET_GUARD_FAKE_TTY=ttysG \
+out="$(TICKET_GUARD_THROTTLE_MIN=0 TICKET_GUARD_STATE_DIR=$TMP TICKET_GUARD_FAKE_PID=777 TICKET_GUARD_FAKE_TTY=ttysG \
TICKET_GUARD_FAKE_ARGV='claude --continue please look at TK-11621 and TK_AGENT=someone' bash "$HOOK")"
ck "FAULT: prose mention of a ticket is not a binding" NUDGE "$out"
# 8. Emitted JSON must be valid and carry the right hook event (a malformed hook is inert)
-out="$(TICKET_GUARD_STATE_DIR=$TMP TICKET_GUARD_FAKE_PID=666 TICKET_GUARD_FAKE_TTY=ttysF \
+out="$(TICKET_GUARD_THROTTLE_MIN=0 TICKET_GUARD_STATE_DIR=$TMP TICKET_GUARD_FAKE_PID=666 TICKET_GUARD_FAKE_TTY=ttysF \
TICKET_GUARD_FAKE_ARGV='claude' bash "$HOOK")"
if printf '%s' "$out" | python3 -c 'import json,sys; d=json.load(sys.stdin); assert d["hookSpecificOutput"]["hookEventName"]=="UserPromptSubmit"; assert "ticket" in d["hookSpecificOutput"]["additionalContext"]' 2>/dev/null; then
pass=$((pass+1)); echo " ok emitted JSON is valid UserPromptSubmit context"
else fail=$((fail+1)); echo " FAIL emitted JSON invalid"; fi
+# 9. INJECTED FAULT: PID REUSE. pid MATCHES the record, but the record was written by
+# a DEAD process that merely held this pid earlier — start times differ. pid alone
+# would re-validate a stale ticket here; pid+start must catch it. (Raised by a
+# second-model review: pid + start time is the unique process identity.)
+mk ttysH '{"owner":{"pid":900,"tty":"ttysH","started":"Sun Sep 13 09:00:00 2026"},"ticket":"TK-11155"}'
+out="$(TICKET_GUARD_THROTTLE_MIN=0 TICKET_GUARD_STATE_DIR=$TMP TICKET_GUARD_FAKE_PID=900 TICKET_GUARD_FAKE_TTY=ttysH \
+ TICKET_GUARD_FAKE_STARTED='Sun Sep 13 16:30:00 2026' TICKET_GUARD_FAKE_ARGV='claude' bash "$HOOK")"
+ck "FAULT: pid recycled from a dead process" NUDGE "$out"
+
+# 10. INJECTED FAULT: headless / no controlling tty. Several ttyless sessions would
+# otherwise collide on one state filename and inherit each other's binding.
+out="$(TICKET_GUARD_THROTTLE_MIN=0 TICKET_GUARD_STATE_DIR=$TMP TICKET_GUARD_FAKE_PID=901 TICKET_GUARD_FAKE_TTY='??' \
+ TICKET_GUARD_FAKE_STARTED='Sun Sep 13 16:30:00 2026' TICKET_GUARD_FAKE_ARGV='claude -p' bash "$HOOK")"
+ck "FAULT: no controlling tty (must not share a slot)" NUDGE "$out"
+
+# 11. INJECTED FAULT: record has no start time at all -> identity UNCONFIRMED -> RED.
+mk ttysI '{"owner":{"pid":902,"tty":"ttysI"},"ticket":"TK-11155"}'
+out="$(TICKET_GUARD_THROTTLE_MIN=0 TICKET_GUARD_STATE_DIR=$TMP TICKET_GUARD_FAKE_PID=902 TICKET_GUARD_FAKE_TTY=ttysI \
+ TICKET_GUARD_FAKE_STARTED='Sun Sep 13 16:30:00 2026' TICKET_GUARD_FAKE_ARGV='claude' bash "$HOOK")"
+ck "FAULT: record carries no start time (unconfirmed identity)" NUDGE "$out"
+
+# 12. THROTTLE: a second nudge for the SAME identity inside the window stays quiet,
+# but the FIRST one always fires. Uses a private stamp dir so it cannot leak.
+STAMP="$TMP/stamps"
+out="$(TICKET_GUARD_THROTTLE_MIN=15 TICKET_GUARD_STAMP_DIR=$STAMP TICKET_GUARD_STATE_DIR=$TMP \
+ TICKET_GUARD_FAKE_PID=903 TICKET_GUARD_FAKE_TTY=ttysJ TICKET_GUARD_FAKE_STARTED='S1' TICKET_GUARD_FAKE_ARGV='claude' bash "$HOOK")"
+ck "throttle: first nudge fires" NUDGE "$out"
+out="$(TICKET_GUARD_THROTTLE_MIN=15 TICKET_GUARD_STAMP_DIR=$STAMP TICKET_GUARD_STATE_DIR=$TMP \
+ TICKET_GUARD_FAKE_PID=903 TICKET_GUARD_FAKE_TTY=ttysJ TICKET_GUARD_FAKE_STARTED='S1' TICKET_GUARD_FAKE_ARGV='claude' bash "$HOOK")"
+ck "throttle: repeat inside window stays quiet" SILENT "$out"
+# A DIFFERENT session on the same tty must NOT be silenced by the old stamp.
+out="$(TICKET_GUARD_THROTTLE_MIN=15 TICKET_GUARD_STAMP_DIR=$STAMP TICKET_GUARD_STATE_DIR=$TMP \
+ TICKET_GUARD_FAKE_PID=904 TICKET_GUARD_FAKE_TTY=ttysJ TICKET_GUARD_FAKE_STARTED='S2' TICKET_GUARD_FAKE_ARGV='claude' bash "$HOOK")"
+ck "throttle: a NEW session is not silenced by the old stamp" NUDGE "$out"
+
echo; echo "pass=$pass fail=$fail"; [ "$fail" -eq 0 ]
diff --git a/ticket-guard.sh b/ticket-guard.sh
index 5877fab..217961c 100755
--- a/ticket-guard.sh
+++ b/ticket-guard.sh
@@ -2,8 +2,8 @@
# ticket-guard.sh — UserPromptSubmit hook. TK-11621.
#
# WHY: CLAUDE.md's hard rule is "every agent ACTION rides a ticket", but the only
-# thing enforcing it was model memory. run-ticket.sh bakes a ticket into sessions
-# it launches; every OTHER spawn (a hand-opened `claude`, an iterm-restore
+# thing enforcing it was model memory. run-ticket.sh bakes a ticket into sessions it
+# launches; every OTHER spawn (a hand-opened `claude`, an iterm-restore
# `claude --continue`) starts unbound and stays unbound silently. TK-11621 measured
# 32 live unbound sessions against 45 bound.
#
@@ -17,44 +17,76 @@
# already-ticketed session nothing, so a false nudge is cheap; a false SILENCE is the
# harmful direction and is what this refuses to produce.
#
-# STALE-SLOT TRAP (memory: tty-is-a-reusable-slot-not-a-session-identity): the store
-# record is keyed by TTY, and a tty is a reusable SLOT. A fresh unbound session can
-# inherit a DEAD session's record still carrying that session's TK-#### — reading it
+# IDENTITY = pid AND start time (2026-09-13, confirmed by a second-model review):
+# the status record is keyed by TTY, and a tty is a reusable SLOT (memory:
+# tty-is-a-reusable-slot-not-a-session-identity), so a fresh unbound session can
+# inherit a DEAD session's record still carrying that session's TK-####. Reading it
# naively yields a false "bound" and silences the guard exactly when it is needed.
-# So a store record is trusted ONLY when its owner pid AND start time match the live
-# session. Verified on ttys073 (record named pid 21464 @11:29 while live pid was
-# 84660 @16:08 — a naive read would have passed a different session's ticket).
+# pid ALONE is not enough — macOS recycles pids, so a dead writer's pid can be
+# re-issued to a live session and re-validate a stale record. pid + start time is the
+# unique process identity. `ps -o lstart=` is byte-identical to the record's
+# owner.started, verified on ttys003.
+# Real stale-slot instance caught: ttys073's record named pid 21464 @11:29 while the
+# live session was pid 84660 @16:08.
#
-# Testability seam (TK-11431 amendment 3) — used ONLY by test-ticket-guard.sh:
-# TICKET_GUARD_STATE_DIR, TICKET_GUARD_FAKE_PID, TICKET_GUARD_FAKE_ARGV,
-# TICKET_GUARD_FAKE_TTY. Hook wiring must NEVER pass these.
+# THROTTLE: a genuinely ticketless session would otherwise get this text on EVERY
+# prompt forever, burning context to repeat itself. It nudges at most once per
+# TICKET_GUARD_THROTTLE_MIN (default 15) per session identity. The FIRST prompt always
+# nudges. This is a deliberate, bounded trade of some silence for not nagging — the
+# stamp is keyed to pid+start, so a NEW session is never silenced by an old one's stamp.
+#
+# KNOWN LIMIT (accepted, documented rather than hidden): step 1 proves a ticket was
+# DECLARED by the launcher, not that the ticket is still OPEN. A session whose ticket
+# was later closed still passes. Verifying liveness would need a `tk` lookup on every
+# prompt, which is exactly the cost this hook exists to avoid.
+#
+# Testability seam (TK-11431 amendment 3) — used ONLY by test-ticket-guard.sh.
+# Hook wiring must NEVER pass these:
+# TICKET_GUARD_STATE_DIR, TICKET_GUARD_STAMP_DIR, TICKET_GUARD_THROTTLE_MIN,
+# TICKET_GUARD_FAKE_PID, TICKET_GUARD_FAKE_ARGV, TICKET_GUARD_FAKE_TTY,
+# TICKET_GUARD_FAKE_STARTED
set -u
STATE_DIR="${TICKET_GUARD_STATE_DIR:-$HOME/.local/state/abrams-terminal-status}"
+STAMP_DIR="${TICKET_GUARD_STAMP_DIR:-$HOME/.local/state/ticket-guard}"
+THROTTLE_MIN="${TICKET_GUARD_THROTTLE_MIN:-15}"
silent(){ exit 0; }
nudge(){
local why="$1"
+ # Throttle: at most one nudge per session identity per THROTTLE_MIN minutes.
+ if [ "$THROTTLE_MIN" -gt 0 ] 2>/dev/null; then
+ local key stamp
+ key="$(printf '%s' "${SELF_TTY:-notty}-${SELF_PID:-0}-${SELF_STARTED:-x}" | tr -c 'A-Za-z0-9._-' '_')"
+ stamp="$STAMP_DIR/$key"
+ mkdir -p "$STAMP_DIR" 2>/dev/null
+ if [ -f "$stamp" ] && [ -z "$(find "$stamp" -maxdepth 0 -mmin "+$THROTTLE_MIN" 2>/dev/null)" ]; then
+ silent # nudged recently for THIS session; stay quiet rather than nag.
+ fi
+ : > "$stamp" 2>/dev/null
+ fi
cat <<JSON
{"hookSpecificOutput":{"hookEventName":"UserPromptSubmit","additionalContext":"[ticket-guard] This session has NO ticket bound (${why}). CLAUDE.md hard rule: every agent action rides a ticket. BEFORE acting, bind one: export TK_AGENT=<short-role-name>; tk inbox; then EITHER tk take TK-<n> if this continues known work, OR tk new \"<short title of what you are actually about to do>\" -p <project>. Then tk log every action and tk comment your reasoning. If a ticket is genuinely already bound, do nothing and proceed. Never invent a ticket for work you are not actually doing. Gated actions (customer-facing / destructive / spend / DNS / publish / send-to-list / remote-push / canonical dw_unified or Shopify writes) still draft to ~/.claude/yolo-queue/pending-approval/ and STOP."}}
JSON
exit 0
}
-# ---- 1. Resolve THIS session's own runtime pid + tty (walk the ppid chain) ----
+# ---- 1. Resolve THIS session's own runtime pid + tty + start time ----
+SELF_PID=""; SELF_TTY=""; SELF_ARGV=""; SELF_STARTED=""
if [ -n "${TICKET_GUARD_FAKE_PID:-}" ]; then
SELF_PID="$TICKET_GUARD_FAKE_PID"; SELF_TTY="${TICKET_GUARD_FAKE_TTY:-ttysTEST}"
- SELF_ARGV="${TICKET_GUARD_FAKE_ARGV:-}"
+ SELF_ARGV="${TICKET_GUARD_FAKE_ARGV:-}"; SELF_STARTED="${TICKET_GUARD_FAKE_STARTED:-}"
else
- SELF_PID=""; SELF_TTY=""; SELF_ARGV=""
p=$$
for _ in 1 2 3 4 5 6 7 8 9 10 11 12; do
line="$(ps -o ppid=,tty=,comm= -p "$p" 2>/dev/null)" || break
set -- $line; pp="${1:-}"; tt="${2:-}"; cm="${3:-}"
case "$cm" in
- *claude*|*codex*) SELF_PID="$p"; SELF_TTY="$tt"
- SELF_ARGV="$(ps -o command= -p "$p" 2>/dev/null)"; break ;;
+ *claude*|*codex*)
+ SELF_PID="$p"; SELF_TTY="$tt"
+ SELF_ARGV="$(ps -o command= -p "$p" 2>/dev/null)"
+ break ;;
esac
[ -z "$pp" ] || [ "$pp" = "0" ] || [ "$pp" = "1" ] && break
p="$pp"
@@ -65,27 +97,52 @@ fi
[ -n "$SELF_PID" ] || nudge "could not resolve this session's own process"
# ---- 2. PRIMARY: launcher-declared ticket in our own argv ----
-# run-ticket.sh bakes `export TK_AGENT=<prefix>-<IDNUM>` where IDNUM is the DRIVING
-# ticket's number alone. Unambiguous by construction (ticket_binding.py AGENT regex).
+# run-ticket.sh bakes `export TK_AGENT=<prefix>-<IDNUM>` as literal text INSIDE the
+# prompt argument, so it is genuinely in argv (verified against live ps output), not
+# merely in the environment. IDNUM is the DRIVING ticket's number alone, which makes
+# it unambiguous by construction (same signal as ticket_binding.py's AGENT regex).
if printf '%s' "$SELF_ARGV" | grep -Eqi 'TK_AGENT=(claude-run|codex-run|local-qwen-27b-run|local-qwen-14b-run|local-qwen-14b-mac1-run)-[0-9]+'; then
silent
fi
-# ---- 3. SECONDARY: identity-checked store record ----
+# ---- 3. SECONDARY: identity-checked status record ----
+# No controlling tty (headless `claude -p`, a detached session): the record is keyed
+# by tty, so without one there is nothing to safely read. Never probe a degenerate
+# path — several ttyless sessions would collide on one filename and inherit each
+# other's binding. Fail loud instead.
+case "$SELF_TTY" in
+ ''|'??'|'?') nudge "no launcher ticket and this session has no controlling tty" ;;
+esac
+
+# LAZY: start time is needed only from here on. 45 of 77 live sessions are bound by
+# the launcher and returned at step 2 above without paying for this extra ps call.
+if [ -z "$SELF_STARTED" ] && [ -z "${TICKET_GUARD_FAKE_PID:-}" ]; then
+ SELF_STARTED="$(ps -o lstart= -p "$SELF_PID" 2>/dev/null | sed 's/^ *//; s/ *$//')"
+fi
+
REC="$STATE_DIR/${SELF_TTY}.json"
[ -f "$REC" ] || nudge "no launcher ticket and no status record for ${SELF_TTY}"
-VERDICT="$(REC="$REC" SELF_PID="$SELF_PID" python3 - <<'PY' 2>/dev/null
+VERDICT="$(REC="$REC" SELF_PID="$SELF_PID" SELF_STARTED="$SELF_STARTED" python3 - <<'PY' 2>/dev/null
import json, os, sys
try:
d = json.load(open(os.environ["REC"]))
except Exception:
- print("UNMEASURED record unreadable"); sys.exit(0)
+ print("UNMEASURED status record is unreadable"); sys.exit(0)
owner = d.get("owner") or {}
-if str(owner.get("pid", "")) != str(os.environ["SELF_PID"]):
- # Stale-slot: this record belongs to a DIFFERENT session that held this tty.
+live_pid, live_started = os.environ["SELF_PID"], os.environ["SELF_STARTED"].strip()
+if str(owner.get("pid", "")) != str(live_pid):
print("UNMEASURED status record belongs to a previous session on this tty")
sys.exit(0)
+rec_started = str(owner.get("started", "")).strip()
+# pid matched — but pids are recycled, so the start time must match too, else this
+# record was written by a DEAD process that merely happened to hold this pid.
+if live_started and rec_started and rec_started != live_started:
+ print("UNMEASURED status record pid was recycled from a previous process")
+ sys.exit(0)
+if not live_started or not rec_started:
+ print("UNMEASURED could not confirm process identity (missing start time)")
+ sys.exit(0)
t = (d.get("ticket") or "").strip()
print("BOUND" if t.upper().startswith("TK-") else "UNBOUND no ticket on this session's record")
PY
← ffe9706 install.sh: gated, idempotent, self-verifying settings.json
·
back to Ticket Guard
·
(newest)