#!/bin/sh # In-Obsidian "Claude is testing" banner + countdown, so the user gets a loud, # in-app heads-up even when they're not watching the Claude chat. # # Why: automated Obsidian looks identical to a normal one. If the user pokes the # UI mid-test, their clicks collide with the automation. The chat πŸ”΄/βœ… handshake # only helps if they're looking at the chat β€” this pushes the warning into # Obsidian itself, with a grace period to back off. # # Usage: # scripts/obs-test-guard start [seconds] # show banner, wait N s (default 10), then "testing now" # scripts/obs-test-guard ensure # INSTANT banner (no wait) β€” re-raise it before any # # driving eval if you're unsure it's still up # scripts/obs-test-guard end # clear banner, show transient "safe to use" notice # scripts/obs-test-guard note 'message' # ad-hoc persistent banner with a custom message # # PROTOCOL: run `start` ONCE at the beginning of a testing session and `end` # ONLY when you're completely done and handing control back. Keep the banner up # across EVERY intervening eval/screenshot/reload β€” do NOT `end` then keep # testing. If you already ended (or aren't sure), call `ensure` (instant, no # countdown β€” the user was already warned) before the next driving eval. # # The banner Notice is persistent (duration 0) and tracked on # window.__claudeTestBanner so `end` can dismiss it. `start` does the wait in the # SHELL (not inside eval β€” obs eval has a ~1s ceiling). DIR="$(cd "$(dirname "$0")" && pwd)" OBS="$DIR/obs" show() { # show β€” (re)uses the single tracked banner Notice. "$OBS" eval code="(() => { const msg = $1; if (window.__claudeTestBanner && typeof window.__claudeTestBanner.setMessage === 'function') { window.__claudeTestBanner.setMessage(msg); } else { window.__claudeTestBanner = new Notice(msg, 0); } return 'shown'; })()" >/dev/null 2>&1 } # verify β€” confirm the CLI is talking to the THROWAWAY dev vault before any # driving/destructive test, so a misrouted CLI (wrong window focused, multiple # vaults open) can never run against a real vault. Two independent checks: # 1. vault name === "Claude Dev Vault" # 2. the sentinel note exists AND carries its unique token # The sentinel makes this robust even if the vault is renamed again β€” a real # vault would have to deliberately contain the token to be mistaken for this one. # Prints "verify: OK" + exits 0 on match; LOUD error + exits 1 otherwise. EXPECT_VAULT="Claude Dev Vault" SENTINEL_TOKEN="claude-dev-vault-7f3a9c2e-do-not-delete" verify() { out="$("$OBS" eval code="(async () => { const name = app.vault.getName(); let token = ''; try { token = await app.vault.adapter.read('CLAUDE-DEV-VAULT-SENTINEL.md'); } catch (e) {} const ok = name === '$EXPECT_VAULT' && token.indexOf('$SENTINEL_TOKEN') !== -1; return JSON.stringify({ ok, name, sentinel: token.indexOf('$SENTINEL_TOKEN') !== -1 }); })()" 2>/dev/null)" case "$out" in *'"ok":true'*) echo "verify: OK β€” $EXPECT_VAULT (name + sentinel confirmed)"; return 0 ;; *) echo "βœ‹ GUARD ABORT: the running Obsidian is NOT the throwaway dev vault." >&2 echo " Expected \"$EXPECT_VAULT\" + sentinel note; got: ${out:-}" >&2 echo " Refusing to drive β€” focus the Claude Dev Vault window (or restore the sentinel) and retry." >&2 return 1 ;; esac } case "$1" in verify) verify; exit $? ;; start) verify || exit 1 secs="${2:-5}" show "'πŸ€– Claude is about to run an automated Stashpad test.\nβœ‹ Please STOP interacting β€” starting in about ${secs} seconds…'" i="$secs" while [ "$i" -gt 0 ]; do sleep 1 i=$((i - 1)) show "'πŸ€– Claude is testing Stashpad.\nβœ‹ Please don\\'t touch β€” starting in ${i}s…'" done show "'πŸ€– Claude is TESTING Stashpad now.\nβœ‹ Please don\\'t touch until you see the all-clear.'" echo "guard: banner up, ${secs}s grace elapsed β€” safe to drive" ;; ensure) # Instant re-raise (no countdown) β€” for follow-up driving evals when the # banner may have been ended. The user was already warned by the session's # initial `start`, so no grace period is needed. Still re-verifies the vault # (the user could have switched windows between evals). verify || exit 1 show "'πŸ€– Claude is TESTING Stashpad now.\nβœ‹ Please don\\'t touch until you see the all-clear.'" echo "guard: banner ensured (instant)" ;; end) "$OBS" eval code="(() => { if (window.__claudeTestBanner && typeof window.__claudeTestBanner.hide === 'function') { try { window.__claudeTestBanner.hide(); } catch (e) {} } window.__claudeTestBanner = null; new Notice('βœ… Safe to use Stashpad again β€” Claude has finished testing.', 0); return 'cleared'; })()" >/dev/null 2>&1 echo "guard: banner cleared, all-clear shown" ;; note) [ -n "$2" ] || { echo "usage: obs-test-guard note 'message'" >&2; exit 1; } # Escape single quotes in the user message for the JS string literal. esc="$(printf '%s' "$2" | sed "s/'/\\\\'/g")" show "'$esc'" echo "guard: custom banner shown" ;; *) echo "usage: obs-test-guard {verify|start [seconds]|ensure|end|note 'message'}" >&2 exit 1 ;; esac