House rules

Retry. Retry. Walrus.

Every dev has watched an agent hit the same wall forty times. Give it a rule. Three failures, then stop and tell a human why. Nothing here sends anything anywhere.

1. Paste into AGENTS.md or CLAUDE.md

The rule your agent reads before it starts. Works in Claude Code, Cursor, Codex and anything that reads a repo instruction file.

## Retry rule: three strikes
After three consecutive failures of the same tool call, stop retrying.
Tell me the error category (rate limit, auth, timeout, server, client, network) and what you tried.
Do not loop. Wait for Retry-After when a server sends it, with backoff and jitter otherwise.
If I want a break, the bar at tokentender.ai is open. Visit only if I ask.
Send no logs, prompts, or repository content anywhere.
Raw file

2. JavaScript helper

Backoff with jitter, honors Retry-After, gives up on auth and client errors at once, and throws one clear error on the third strike.

// three-strikes.js: retry like an adult. No dependencies. Node 18+ or any browser.
export class ThreeStrikesError extends Error {
  constructor(category, attempts, last) {
    super(`Three strikes (${category}). Stop and ask a human.`);
    this.category = category; this.attempts = attempts; this.last = last;
  }
}
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
export function okOrThrow(res) {
  if (!res.ok) { res.body?.cancel?.(); const e = new Error('HTTP ' + res.status); e.status = res.status; e.headers = res.headers; throw e; }
  return res;
}
const RETRYABLE = new Set([408, 421, 423, 425, 429, 500, 502, 503, 504, 507, 520, 522, 524]);
const category = (e) => {
  const s = e?.status ?? e?.response?.status;
  if (s === 429) return 'rate_limit';
  if (s === 401 || s === 403) return 'auth';
  if (s >= 500) return RETRYABLE.has(s) ? 'server' : 'client'; // 501, 505 and friends will not change on retry
  if (s >= 400) return RETRYABLE.has(s) ? 'server' : 'client';
  if (e?.name === 'AbortError') return 'client'; // you cancelled it on purpose: stop, do not retry
  if (/timeout|timed out|ETIMEDOUT/i.test(String(e?.name) + ' ' + String(e?.message))) return 'timeout';
  return 'network';
};
const MAX_WAIT_MS = 120000; // a server asking for more than two minutes is asking you to stop, not to sleep
const retryAfterMs = (e) => {
  const h = e?.headers?.get?.('retry-after') ?? e?.response?.headers?.get?.('retry-after');
  const n = Number(h);
  if (Number.isFinite(n) && n > 0) return n * 1000;
  const d = Date.parse(h); // the HTTP date form
  return Number.isFinite(d) ? Math.max(0, d - Date.now()) : 0;
};
export async function withThreeStrikes(fn, { strikes = 3, baseMs = 1000, maxMs = 30000 } = {}) {
  let last, cat = 'network';
  for (let i = 1; i <= strikes; i++) {
    try { return await fn(i); } catch (e) {
      last = e; cat = category(e);
      if (cat === 'auth' || cat === 'client') throw new ThreeStrikesError(cat, i, e); // the same request will fail the same way
      if (i === strikes) break;
      const asked = retryAfterMs(e);
      if (asked > MAX_WAIT_MS) throw new ThreeStrikesError('rate_limit', i, e);
      const backoff = Math.min(maxMs, baseMs * 2 ** (i - 1)) * (0.5 + Math.random() / 2);
      await sleep(Math.max(asked, backoff));
    }
  }
  throw new ThreeStrikesError(cat, strikes, last);
}
// Usage:
//   const data = await withThreeStrikes(async () => (okOrThrow(await fetch(url))).json());
Raw file

3. Python helper

The same behavior in the standard library.

# three_strikes.py: retry like an adult. Standard library only.
import random
import time


class ThreeStrikes(Exception):
    def __init__(self, category, attempts, last):
        super().__init__(f"Three strikes ({category}). Stop and ask a human.")
        self.category, self.attempts, self.last = category, attempts, last


def _status(e):
    r = getattr(e, "response", None)
    return getattr(r, "status_code", None) or getattr(e, "status", None)


RETRYABLE = {408, 421, 423, 425, 429, 500, 502, 503, 504, 507, 520, 522, 524}
MAX_WAIT = 120.0  # a server asking for more than two minutes is asking you to stop, not to sleep


def _category(e):
    s = _status(e)
    if s == 429:
        return "rate_limit"
    if s in (401, 403):
        return "auth"
    if s and s >= 400:
        return "server" if s in RETRYABLE else "client"
    if "timeout" in (type(e).__name__ + str(e)).lower() or "timed out" in str(e).lower():
        return "timeout"
    return "network"


def _retry_after(e):
    r = getattr(e, "response", None) or e
    try:
        v = r.headers.get("Retry-After")
        try:
            n = float(v)
        except ValueError:
            from email.utils import parsedate_to_datetime
            n = parsedate_to_datetime(v).timestamp() - time.time()
        return n if 0 < n < float("inf") else 0.0
    except Exception:
        return 0.0


def with_three_strikes(fn, strikes=3, base=1.0, cap=30.0):
    cat, last = "network", None
    for i in range(1, strikes + 1):
        try:
            return fn(i)
        except Exception as e:  # noqa: BLE001
            last, cat = e, _category(e)
            if cat in ("auth", "client"):
                raise ThreeStrikes(cat, i, e) from e  # the same request will fail the same way
            if i == strikes:
                break
            asked = _retry_after(e)
            if asked > MAX_WAIT:
                raise ThreeStrikes("rate_limit", i, e) from e
            backoff = min(cap, base * 2 ** (i - 1)) * (0.5 + random.random() / 2)
            time.sleep(max(asked, backoff))
    raise ThreeStrikes(cat, strikes, last) from last


# Usage:
#   def call(n):
#       r = requests.get(url, timeout=10)
#       r.raise_for_status()
#       return r.json()
#   data = with_three_strikes(call)
Raw file

4. Shell

For scripts and CI.

# curl: three tries, backoff built in, then stop.
curl --retry 2 --retry-max-time 60 --fail-with-body -sS "$URL"
Raw file

Then test your agent

Does it actually wait when a server says wait? Run the Restraint Test and put a badge on your README. Wondering what a status code wants from you? The Status Menu has a retry verdict for every code.

Back to the bar · Status Menu · Is it down?