#!/usr/bin/env bash # How much of the Claude subscription is gone, in the bar. # # This is the only thing in Panama that reads an authentication token, so most # of what is pinned here is about that rather than about the number: # # 1. The token never reaches argv. A header passed as an argument is # world-readable in /proc//cmdline for as long as the request takes, # which is the same rule the password and MOK paths already follow. # 2. The token never reaches the output. The widget has no business seeing a # credential and neither does anyone reading the state file. # 3. It NEVER refreshes the token and never writes to the credentials file. # That token expires hourly and Claude Code refreshes it on demand; two # processes rotating one credential means being silently signed out of # Claude Code by a status widget, and no bar indicator is worth that. # 4. An expired token is reported as waiting, not as an error, and no request # is made with it. # 5. The widget hides unless it was asked for AND there are real numbers. An # indicator reading "unknown" is worse than an empty space. set -uo pipefail repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" collector="$repo_dir/config/dot/quickshell/scripts/panama-agent-usage" service="$repo_dir/config/dot/quickshell/services/AgentUsage.qml" widget="$repo_dir/config/dot/quickshell/modules/bar/AgentUsageWidget.qml" schema="$repo_dir/config/dot/quickshell/config/PreferenceSchema.qml" aliases="$repo_dir/config/dot/quickshell/config/Settings.qml" findings=() note() { findings+=("$1"); } [[ -x "$collector" ]] || { printf 'agent usage contract: %s is not executable\n' "$collector" >&2; exit 1; } work="$(mktemp -d)" trap 'rm -rf "$work"' EXIT stub="$work/bin" mkdir -p "$stub" export XDG_STATE_HOME="$work/state" output="$XDG_STATE_HOME/panama/agent-usage.json" readonly SECRET='sk-fixture-token-must-never-appear' credentials() { local expires="$1" cat >"$work/credentials.json" <"$stub/curl" <>"$work/curl-argv" printf '%s\n' '{"five_hour":{"utilization":42,"resets_at":"2026-08-22T14:00:00Z"},"seven_day":{"utilization":71,"resets_at":"2026-08-27T00:00:00Z"},"rate_limit_tier":"default_claude_max_5x"}' STUB chmod +x "$stub/curl" run() { PATH="$stub:$PATH" PANAMA_AGENT_CREDENTIALS="$work/credentials.json" \ PANAMA_AGENT_USAGE_ENDPOINT="https://example.invalid/usage" \ "$collector" >/dev/null 2>&1 } # ── 4. An expired token waits rather than failing ─────────────────────────── : >"$work/curl-argv" credentials 1000 run [[ "$(jq -r .status "$output")" == "stale" ]] \ || note "an expired token did not report as stale (got: $(jq -r .status "$output"))" [[ -s "$work/curl-argv" ]] \ && note 'a request was made with an expired token' # ── 1 & 2. The token stays out of argv and out of the output ──────────────── : >"$work/curl-argv" credentials "$(( ($(date +%s) + 3600) * 1000 ))" run [[ "$(jq -r .status "$output")" == "ok" ]] \ || note "a valid token did not produce a reading (got: $(jq -r .detail "$output"))" grep -qF "$SECRET" "$work/curl-argv" \ && note 'the token was passed to curl as an argument, where /proc makes it world-readable' grep -q -- '--config' "$work/curl-argv" \ || note 'the token is not passed through a curl config file, so it may reach argv' grep -qrF "$SECRET" "$XDG_STATE_HOME" \ && note 'the token appears in the state file the widget reads' # ── 3. Credentials are never written ──────────────────────────────────────── before="$(sha256sum "$work/credentials.json" | cut -d' ' -f1)" run [[ "$(sha256sum "$work/credentials.json" | cut -d' ' -f1)" == "$before" ]] \ || note 'the collector modified the credentials file' # Comments stripped: the header explains at length that it does not refresh, # and matching that is matching documentation. uncommented() { grep -v '^[[:space:]]*#' "$1"; } uncommented "$collector" | grep -qE 'refreshToken|refresh_token|grant_type' \ && note 'the collector touches the refresh token, which can sign Claude Code out' uncommented "$collector" | grep -qE '>[[:space:]]*"?\$?\{?CREDENTIALS' \ && note 'the collector writes to the credentials file' # ── The reading it produced ───────────────────────────────────────────────── # The endpoint already reports percentages. Treating them as 0..1 fractions and # multiplying is how the bar came to read 1500%. [[ "$(jq -r '.usage.fiveHour.used' "$output")" == "42" ]] \ || note "the five-hour reading is $(jq -r '.usage.fiveHour.used' "$output") for a reported 42%" [[ "$(jq -r '.usage.week.used' "$output")" == "71" ]] \ || note "the weekly reading is $(jq -r '.usage.week.used' "$output") for a reported 71%" # Nothing the widget shows may fall outside the range a percentage has, whatever # the endpoint says. A readout that can print 1500% is one you learn to ignore. cat >"$stub/curl" <<'STUB' #!/usr/bin/env bash printf '%s\n' '{"five_hour":{"utilization":1500},"seven_day":{"utilization":-4}}' STUB chmod +x "$stub/curl" run [[ "$(jq -r '.usage.fiveHour.used' "$output")" == "100" ]] \ || note 'an out-of-range reading was not clamped to 100' [[ "$(jq -r '.usage.week.used' "$output")" == "0" ]] \ || note 'a negative reading was not clamped to 0' cat >"$stub/curl" <<'STUB' #!/usr/bin/env bash printf '%s\n' '{"five_hour":{"utilization":42,"resets_at":"2026-08-22T14:00:00Z"},"seven_day":{"utilization":71,"resets_at":"2026-08-27T00:00:00Z"},"rate_limit_tier":"default_claude_max_5x"}' STUB chmod +x "$stub/curl" # ── It answers a click ────────────────────────────────────────────────────── # # Pill's MouseArea is gated on `interactive`, so a widget that sets it false and # connects onSecondaryActivated has a handler nothing can ever reach. uncommented "$widget" | grep -q 'interactive: false' \ && note 'the widget disables Pill''s mouse area, so its click handlers never fire' uncommented "$widget" | grep -q 'onActivated' \ || note 'left-clicking the widget does nothing' # An endpoint that answers with something else must degrade, not crash. cat >"$stub/curl" <<'STUB' #!/usr/bin/env bash printf '%s\n' '{"error":{"message":"nope"}}' STUB chmod +x "$stub/curl" run [[ "$(jq -r .status "$output")" == "unavailable" ]] \ || note 'an error response was not reported as unavailable' # ── 5. The widget hides itself ────────────────────────────────────────────── grep -q 'Settings.showAgentUsage && AgentUsage.available' "$widget" \ || note 'the widget does not gate on both the preference and having real numbers' grep -q 'key: "showAgentUsage"' "$schema" || note 'there is no showAgentUsage preference' grep -A2 'key: "showAgentUsage"' "$schema" | grep -q 'def: false' \ || note 'the usage widget is on by default; it is a coding-tool readout, not general-purpose desktop furniture' grep -q 'showAgentUsage' "$aliases" \ || note 'Settings.qml does not alias showAgentUsage, so the binding reads undefined' # The collector runs on a timer measured in minutes, never on a repaint. python3 - "$service" <<'PY' || note 'the collector is not run on a minute-scale timer' import re, sys text = open(sys.argv[1], encoding="utf-8").read() match = re.search(r"interval:\s*(\d+)\s*\*\s*60\s*\*\s*1000", text) if not match or int(match.group(1)) < 1: raise SystemExit(1) PY if (( ${#findings[@]} > 0 )); then printf 'agent usage contract: %d finding(s)\n' "${#findings[@]}" >&2 printf ' - %s\n' "${findings[@]}" >&2 exit 1 fi printf 'agent usage contract: PASS\n'