#!/bin/sh # grokbot-guard — a stop button and a budget for work your Grok Bot runs in the background. # # WHY THIS EXISTS, in the owner's words rather than mine. # # @drpguerin, 26 Aug 2026: "Overnight: agents ran wild, exhausted the full weekly quota, # production site went down" and "Mobile: no way to monitor or pause agents, only delete". # # Background work on a Bot's computer keeps running after you close the app. That is the feature. # The missing half is a way to stop it that is not deleting the Bot, and a ceiling it cannot pass # while you are asleep. This is that half, in one file, using only what is already on the machine. # # USE # sh guard.sh mywork --max-runs 50 --max-minutes 120 -- your-command --args # runs your command over and over, and stops at 50 runs, at 120 minutes, or the moment the # stop file appears, whichever comes first. # touch /tmp/grokbot-STOP # the stop button. Every guarded loop on the machine halts. # touch /tmp/grokbot-STOP-mywork # stops only this one. # sh guard.sh --status mywork # runs so far, minutes elapsed, what is left, why it ended. # # The stop file is a FILE on purpose. You can create it from anything that can touch a path: another # Bot, a one line message to a Bot, a file manager on your phone. It does not need this script to be # running, it does not need a pid, and it survives the thing it is stopping. # # It stops BETWEEN runs of your command, never in the middle of one. A guard that kills work halfway # is how you get a half written file, and that is worse than the thing it was protecting you from. set -eu DIR=${GROKBOT_GUARD_DIR:-/tmp} now() { date -u +%Y-%m-%dT%H:%M:%SZ; } usage() { echo "usage: sh guard.sh [--max-runs N] [--max-minutes N] [--every S] -- " echo " sh guard.sh --status " exit 2 } [ $# -ge 1 ] || usage if [ "$1" = "--status" ]; then [ $# -ge 2 ] || usage st="$DIR/grokbot-guard-$2.state" if [ ! -f "$st" ]; then echo "$2: never run"; exit 0; fi cat "$st" exit 0 fi name=$1; shift max_runs=0 max_minutes=0 every=0 while [ $# -gt 0 ]; do case "$1" in --max-runs) max_runs=$2; shift 2 ;; --max-minutes) max_minutes=$2; shift 2 ;; --every) every=$2; shift 2 ;; --) shift; break ;; *) usage ;; esac done [ $# -ge 1 ] || usage log="$DIR/grokbot-guard-$name.log" state="$DIR/grokbot-guard-$name.state" stop_all="$DIR/grokbot-STOP" stop_one="$DIR/grokbot-STOP-$name" started=$(date -u +%s) runs=0 reason=running write_state() { elapsed=$(( $(date -u +%s) - started )) { echo "name $name" if [ "$max_runs" -gt 0 ]; then echo "runs $runs of $max_runs"; else echo "runs $runs, no run budget"; fi if [ "$max_minutes" -gt 0 ]; then echo "minutes $(( elapsed / 60 )) of $max_minutes"; else echo "minutes $(( elapsed / 60 )), no time budget"; fi echo "ended $reason" echo "stop touch $stop_all (or $stop_one)" } > "$state" } echo "$(now) START name=$name max_runs=$max_runs max_minutes=$max_minutes cmd=$*" >> "$log" write_state while :; do # The stop button is checked BEFORE each run, so a stop that lands while your command is working # takes effect at the next boundary rather than corrupting the current one. if [ -e "$stop_all" ] || [ -e "$stop_one" ]; then reason=stopped echo "$(now) STOPPED name=$name after=$runs runs" >> "$log" break fi if [ "$max_runs" -gt 0 ] && [ "$runs" -ge "$max_runs" ]; then reason=run_budget echo "$(now) BUDGET name=$name reason=max_runs runs=$runs" >> "$log" break fi if [ "$max_minutes" -gt 0 ]; then elapsed_min=$(( ( $(date -u +%s) - started ) / 60 )) if [ "$elapsed_min" -ge "$max_minutes" ]; then reason=time_budget echo "$(now) BUDGET name=$name reason=max_minutes minutes=$elapsed_min" >> "$log" break fi fi runs=$(( runs + 1 )) echo "$(now) RUN name=$name n=$runs" >> "$log" # A failing command must not kill the guard, otherwise one bad network call ends the supervision # and the work carries on unsupervised, which is the exact situation this exists to prevent. "$@" || echo "$(now) COMMAND_FAILED name=$name n=$runs status=$?" >> "$log" write_state [ "$every" -gt 0 ] && sleep "$every" done write_state echo "$(now) END name=$name runs=$runs reason=$reason" >> "$log"