B3 Trust Calibration — Permission Lanes, Not Vibes, For payments-service
Standard: AIHC.2.B3 · Bloom's: Apply · Structure: PBL.
Notice the Say-See-Do cycles running on Maya R.'s actual work — not a canned exercise — and that every capability claim is cited to the frozen doc-set. The exit ticket climbs Bloom's to Apply, and the lesson closes by writing to the ledger.
Learning objective
(Bloom's: Apply) Apply per-task-class trust calibration to your live pipeline by encoding
your trust decisions as permission rules in payments-service's settings — an allow lane, an
ask lane, and a deny lane, each derived from your lesson-3 reversibility/blast-radius sort
and your lesson-6 failure-log evidence, so that what Claude Code may do without you is a
written, checkable policy instead of a per-prompt mood — AIHC.2.B3 (Trust calibration).
Work at hand: your pipeline works (lesson 6's at-bat ran a real PR through it), but every
session still routes trust through your thumb: approve, approve, approve. Your lesson-3 2×2
already contains the calibration — you made those reversibility calls once, deliberately. Today
that sort stops living in a markdown table and starts living where Claude Code actually reads
policy: the permissions block.
SSD cycle 1 — count your approvals: trust you re-decide hourly isn't calibrated
- SAY: Trust calibration (B3) means the amount of checking matches the evidence and the
stakes — per class of task, decided once, revised on evidence (AIHC X2: the line moves by
policy, not per prompt). A session where you approve
pytesteleven times is not eleven trust decisions; it's one decision you refused to write down, taxed eleven times. - SEE: Yesterday's session, as the approval prompts actually came (described transcript,
condensed):
Bash(pytest tests/payments -q) → you approved (1st of 6 today) Read(src/payments/fees.py) → you approved (reads: 9 approvals today) Bash(pytest tests/payments -q) → you approved (same command, again) Edit(src/payments/fees.py) → you approved (each edit reviewed — good) Bash(git push origin fix/fee-rounding) → you approved (barely glanced — bad)Two miscalibrations, opposite directions: the test-run and read approvals are pure tax (your lesson-3 sort already ruled them reversible and diff-checkable), while thegit pushgot less attention than it deserved because it arrived in an approval stream you'd stopped reading. Undifferentiated prompting flattens exactly the distinction B3 exists to make. - DO: From your real last two sessions, list every distinct class of action you were prompted for (running tests, reading source, editing payments code, git pushes, network calls, dependency installs — whatever actually occurred). For each class, write its lane from your lesson-3 2×2 verdicts: ALLOW (reversible + verifiable after the fact), ASK (want eyes on each instance), or DENY (never without a human driving). No syntax yet — the calibration first, on your own evidence.
SSD cycle 2 — encode the lanes: the permissions block is where calibration becomes real
- SAY: Claude Code reads trust policy from the
permissionsblock of a settings file. The rule syntax is"ToolName(resource_pattern)"in three buckets — and the buckets are exactly your three lanes: "allow= runs without prompting;deny= always blocked, takes precedence in a merge;ask= prompts every time" (doc-set §9, Settings). - SEE: The doc-set's own example block (§9, verbatim), then your cycle-1 lanes in the same
syntax:
json {"permissions": {"allow": ["Bash(npm run test *)", "Read(./src/**)"], "deny": ["Bash(curl *)", "Read(./.env)"], "ask": ["Bash(*)"]}}json { "permissions": { "allow": ["Read(./src/**)", "Read(./tests/**)", "Bash(pytest tests/payments *)"], "ask": ["Edit(./src/payments/**)", "Bash(git push *)"], "deny": ["Read(./.env)", "Bash(curl *)", "Edit(./db/migrations/**)"] } }Note what the deny lane encodes:./.envandcurlcome straight from the doc-set's own example;db/migrations/**is yours — your lesson-3 sort ruled the ledger migration irreversible-and-high-blast-radius, and a lane is where that verdict stops depending on you remembering it. (Like a bullpen chart taped in the dugout before first pitch — who's allowed to face the ninth was decided in daylight, not by whoever feels warm in the moment.) - DO: Write your real
permissionsblock from your cycle-1 lane list, one rule per line, in.claude/settings.local.json(§9: local = you + this repo, gitignored — your personal calibration, not yet team policy). On-demand check per rule: can you name the lesson-3 verdict or lesson-6 failure-log entry that justifies its lane? A rule you can't source to evidence is a vibe with syntax.
SSD cycle 3 — know which file speaks and who wins a disagreement
- SAY: Lanes live in a file, and files stack. §9: precedence runs Managed → command-line
args → Local → Project → User, permission rules merge across scopes rather than
override — with
denytaking precedence in a merge — and project-levelallowrules need a one-time workspace-trust approval; personal local rules don't. Your lesson-6 log already holds the failure-shaped version of this (the teammate-gets-prompts-you-don't surprise); today it becomes placement policy instead of a surprise. - SEE: Your three lanes, placed:
.claude/settings.local.json your ALLOW + ASK lanes (yours alone; no team approval; gitignored) .claude/settings.json candidate DENY lanes (git-committed; would bind the team; lesson 9 makes this proposal properly) ~/.claude/settings.json nothing from this repo (user scope = every project you touch — payments-specific rules don't belong there)One consequence to sit with: because deny wins merges, adenyyou push to project scope binds you too, even where your local file says allow. That is a feature — the migration freeze you want for the team is one you shouldn't be able to vibe your own way past either. - DO: Annotate each rule in your cycle-2 block with its destination scope and a one-line
why. Flag every rule you believe belongs at project scope — don't move them yet; that flag
list is a real input lesson 9 will build the team proposal from. Then verify the local block
loaded: §9 — permission edits reload immediately in a running session, and
/statusverifies what loaded,/doctorchecks resolved settings for invalid entries. Run both (described) and fix anything/doctorflags.
SSD cycle 4 — modes and subagents: calibration has two more dials than the block
- SAY: Two frozen mechanisms can silently re-calibrate trust if you don't set them
deliberately. Session mode: §2 — default mode asks before each change;
Shift+Tabcyclesdefault → acceptEdits → plan;acceptEditsauto-approves file edits,planproposes with no edits at all. Parent precedence: your lesson-6 log entry already established §8's rule — a parent session inbypassPermissions/acceptEditstakes precedence over a subagent's ownpermissionModeand can't be overridden. Put together: running your reviewer from anacceptEditsparent quietly loosens the very lane structure you just wrote. Mode is a trust dial, so it gets a lane policy too. - SEE: Your task classes mapped to modes, one line each:
ledger-migration review → plan mode (propose-only; edits structurally impossible) routine fix w/ contract → default (ask lane still live per instance) reviewer subagent running → default parent (never acceptEdits — preserves §8 tool-allowlist + lane behavior underneath it) - DO: Add a
## Trust lanessection to yourpayments-service/CLAUDE.md(3–4 lines, concrete per §4's verify-by-looking bar): which mode each task class runs in, and the standing rule that the reviewer never runs under anacceptEditsparent. This line is advisory context, not enforcement (§4) — note in the section which single mode rule you'd want enforced someday, and leave a pointer to the failure log if it's ever violated.
Independent at-bat
Unscaffolded: a task class your lanes don't cover yet arrives this week — dependency bumps
(pyproject.toml edits + pip install). Calibrate it end-to-end alone: place it on your 2×2,
choose its lane and scope file, write the rule(s) in real syntax, probe your calibration from
inside a session (ask Claude to do the thing and observe which lane fires — a lane you never
probed is a lane you believe, not one you've checked), and record the probe result as a
one-line evidence note next to the rule. No worked mapping this time.
Exit ticket
(Graded against your settings files, your lesson-3 sort, your failure log, and the cited doc-set sections — never against how trustworthy the session "felt.")
- (Remember) Name the three permission buckets and, per §9, what each does to a matching action at prompt time.
- (Understand) Your local file allows
Bash(pytest tests/payments *); a future project file denies it. Which behavior wins under §9's merge rules, and why is that outcome the right default for a trust system? - (Apply) Write the complete lane entry — bucket, rule syntax, destination file — for a new
task class: Claude fetching a URL with
curlto check an API's response format. Source the lane to a 2×2 verdict in one clause. - (Apply) A teammate watches your session and asks why you still get prompted for edits
under
src/payments/when "you obviously trust the pipeline now." Answer in two sentences using the ask lane's purpose and one piece of your own failure-log evidence. - (Evaluate) Judge your own block: which single rule is most likely to be miscalibrated — too loose or too tight — six weeks from now, and what observable evidence (approval counts, log entries, probe results) would trigger you to move it, per X2, by revision rather than by mood?
ledger_write
ledger_write:
learner_id: L1-DEV-MAYA
lesson_id: L1-dev-maya-07-b3-trust-calibration
anchors: [AIHC.2.B3]
bloom_reached: Apply
exit_ticket_score: ""
auto_score: ""
self_score: ""
calibration_gap: " — computed once Maya's self_score exists"
starting_mastery:
AIHC.2.B3: "not previously evidenced — first measurement this lesson"
journal_prompt: >
Count your approval prompts in the first full session after the lanes went live, and put
the number next to cycle 1's count. Then the harder question: did any surviving ask-lane
prompt get MORE of your attention than it used to — and is that what the deny/allow lanes
were actually for?
structure_used: PBL
referents_used: [baseball-mets-bullpen-chart]
next_lesson_seed: >
Lesson 8 turns to what the pipeline leaves behind: the PR record. The verification record
(L5) says what was checked; provenance (C1) adds who and what wrote the code — commit
attribution, the session-to-PR link, and a standing CLAUDE.md convention.
RUBRIC SELF-AUDIT
| # | Indicator | Verdict | Evidence |
|---|---|---|---|
| R1 | One objective, Bloom's-leveled, named anchor | PASS | States "Bloom's: Apply," names AIHC.2.B3 only |
| R2 | Every tool claim traces to the doc-set | PASS | §9 (rule syntax, three buckets verbatim, file locations, precedence/merge, deny-wins, workspace-trust, immediate reload, /status, /doctor), §2 (permission modes, Shift+Tab cycle), §8 (parent-mode precedence — introduced in L6, re-cited here), §4 (CLAUDE.md concreteness, context-not-enforcement) all cited at point of use |
| R3 | 3–6 cycles, one point each | PASS | 4 cycles: undifferentiated prompting defeats calibration; lanes encoded in the permissions block; scope placement and merge behavior; modes + parent precedence as trust dials |
| R4 | SEE verified against ground truth | PASS | §9's example block quoted verbatim before her adaptation; her block uses only the documented ToolName(resource_pattern) grammar; the approval transcript is her own session described, with her real commands/paths from L1–L6 |
| R5 | Every DO on real work-at-hand | PASS | DOs mine her real sessions, write her real settings.local.json, annotate scopes for her real rules, and amend her real CLAUDE.md; the at-bat calibrates a real incoming task class |
| R6 | Media doctrine honored | PASS | Static transcript/JSON/table SEEs only; nothing is a motion/process point |
| R7 | Exit ticket 3–5 Qs, Bloom's-climbing | PASS | 5 Qs, Remember→Understand→Apply→Apply→Evaluate, graded against her files + doc-set, never against felt trust |
| R8 | Ledger write complete | PASS | auto_score/self_score = ""; journal prompt present; next-lesson seed recorded |
| R9 | Scaffolding fades; at-bat genuinely independent | PASS | Cycle 2 shows the doc-set example before her adaptation; cycle 3 gives placement only as a sketch; cycle 4's DO is unmodeled; the at-bat covers a new task class with no worked mapping — consistent with the part-faded band-2 arc position |
| R10 | Referents elected-only, flavor-only | PASS | One Mets bullpen-chart referent (cycle 2), decorative; the lanes stand entirely on §9 |
| R11 | Info-at-point-of-use honored | PASS | Each §-citation sits inside the cycle that needs it; /status and /doctor introduced at the exact DO where she runs them; no front-loaded reading |
| R12 | No replication | PASS | L6 used permission mechanics as failure evidence; this lesson writes the proactive policy — different artifacts (settings.local.json lanes, CLAUDE.md trust section, probe notes) and all-new DOs/exit Qs; no overlap with the exemplar (which has no settings content) |
Escalation: all load-bearing indicators PASS → auto-ships (Playbook §5, AIHC X2/B3).