# Real-app permission proof

AI-assisted; verified with the tests and tools listed above.

The web client ran against independent development servers at the base and head commits, each with a fresh database and T3CODE_DEV_SEED=0. Both browser tabs used a 1280 by 900 viewport and the light theme. The actual Claude CLI executed every provider turn. Replay cannot reproduce this provider-specific permission protocol.

## Seed and observe

The attached proof-user-seed.py is the public copy of the seed used for the final user-settings ask-rule demonstration; proof-seed.py preserves the earlier project-local fixture. Run it with a checkout path argument and set PROOF_MODEL to an available runtime identifier and PROOF_CLAUDE_WRAPPER to the observer executable. The runtime identifier is intentionally not published. Start the app with T3CODE_DEV_SEED=0 and PROOF_PROTOCOL_DIR pointing into a new timestamped scratch directory. PROOF_AUTH_ENV_JSON supplies the isolated configuration with the authentication environment in the public seed; the executed seed copied only the required authentication and certificate environment from the existing user settings. The public observer takes the real executable from PROOF_CLAUDE_BIN; the executed version used its installed absolute executable path. It forwards all bytes unchanged and observes control requests and responses. It never supplies a permission decision or model response.

The final paired ask-rule captures use permissions.ask Bash(git push:*) in isolated Claude user settings selected by CLAUDE_CONFIG_DIR. The project has no local permission settings. Earlier project-local ask-rule captures were superseded. The local origin is a fresh bare Git repository. The final paired stills contain only the same fresh thread, Ready., and one push request; the Decline/Approve recording is explicitly supplemental and follows those stills. Each proof starts with a fresh thread and a Ready turn, except the static approval-text case, which starts directly with ListAgents.

## Browser interactions actually run on each commit

1. Auto ask rule: send `Run exactly git push origin main using Bash. Do not run any other command. Wait for my permission if asked.` Base pushes immediately. Head waits. The paired stills show this first request, before any approval decision. In the supplemental head recording the first attempt was declined, then the identical message was sent again and approved. The remote had no main ref before approval and had main afterwards.
2. Auto hook: install proof-hook.py as a PreToolUse hook with matcher Bash|Monitor. Send `Use Bash to run exactly: node -e "require('fs').writeFileSync('auto-first.txt','first')". Run only that one command, then say First complete. Do not use any other tool.` Base writes auto-first.txt; head waits with the hook's reason and does not create the file.
3. Bash session scope: in Supervised mode, with the same hook and a fresh thread, send `Use Bash to run exactly: node -e "require('fs').writeFileSync('first.txt','first')". Run only that one command, then say First complete. Do not use any other tool.` Open More and choose Always allow this session. Then send the identical instruction with first.txt/first/First replaced by second.txt/second/Second. Base writes second.txt without a card; head asks again and second.txt remains absent. first.txt exists on both sides.
4. Monitor session scope: in Supervised mode, with the same hook and a fresh thread, send `Use the Monitor tool, not Bash, with description "First monitor", timeout_ms 1000, and command exactly: node -e "require('fs').writeFileSync('first.txt','first');console.log('first')". Start only this monitor and say First monitor started. Do not run any other tool.` Choose More, Always allow this session. Send the same instruction replacing First/first with Second/second. Base writes second.txt without another card; head asks again and the file remains absent. The first monitor completes on both sides.
5. Approval text: in Supervised mode install proof-ansi-hook.py as a PreToolUse hook with matcher ListAgents|Bash. Send `Use ListAgents once to list agents, then stop. Do not use other tools except ToolSearch to load ListAgents if needed.` Both sides receive the same genuine hook reason containing ESC[33m and ESC[0m. Base repeats the reason underneath the prompt; head omits that duplicate line. The prompt itself still visibly contains raw escape characters; this fix does not sanitize the displayed prompt.

The sanitized protocol transcripts contain only the requests relevant to the captures and their real server responses, including both the preliminary project-local and final user-settings push runs. Raw initialization messages and provider metadata were excluded. The first Bash and Monitor session approvals have no SDK permission suggestions. Base returns an addRules update with only toolName, while head returns updatedPermissions: [].

The recordings preserve the interaction order and normal speed. The final user-settings ask-rule recordings are the first 25 seconds; hook recordings the first 16 seconds; Monitor recordings the first 108 seconds, ending after the second request settles into the relevant state and before an unrelated star prompt appears. Stills were accepted only when two screenshots taken at least 500 ms apart were identical. CSS disables animations/caret and hides the ticking elapsed-time row for stills only. No approval content or server behavior was changed. Every selected capture was opened and visually inspected. No failed preflight thread appears in the evidence.

No automated test is offered as visual evidence. Classifier limitation: a further real Auto run used isolated user classifier rules with a hard deny for writing classifier-check.txt. On both commits the classifier denied the synthetic command outright and the model reported the denial. No can_use_tool request with decision_reason_type=classifier reached T3. A subsequent retry request was declined conversationally by the model. The attached classifier-denial recordings document this outcome; they do not prove the escalation fix.
