Poka-Yoke
rainmanjam v0.2.0
Publisher description
From the marketplace listing
Mistake-proof your code, your pipeline, and your agents. Audits code for mistakes that are possible, designs APIs where misuse cannot be expressed, installs guardrails, and turns incidents into devices that prevent recurrence.
Language: English · Automatically detected from descriptions.
Publisher keywords
Search terms declared by the publisher.
Show all 24 keywords
Matches for “code-review”
Exact text from the indicated source. A mention alone does not establish support for your task.
Publisher keywords · listing
code-review defensive design defensive-design error proof error-prevention fail safe foolproof footgun guardrails hardening idiot proof invalid states unrepresentable lean mistake proof mistake-proofing poka-yoke poke yoke poke-yoke reliability shigeo shingo shingo toyota production system type-safety zero defects
Files & skills
File archives
Skill instructions
agent-guardrails9.16 KB
---
name: agent-guardrails
description: >-
Stop an AI agent damaging your repo: PreToolUse hooks, permission deny rules, protected paths, verification gates. Use when "claude keeps force pushing", "CLAUDE.md says X but it still does Y", "stop the agent touching prod or .env", or making a repo safe for unattended agent work. For AI features you ship to users use llm.
---
# Poka-Yoke for AI-Written Code
An agent is a fast, tireless operator with no memory of yesterday and a strong prior toward
appearing successful. That is the exact profile Shingo designed poka-yoke for, except an
agent makes mistakes faster than any human, and never learns from the ones you correct in
conversation.
The governing insight: **instructions to an agent are rung zero.** A line in CLAUDE.md saying
"never commit to main" is training, and training degrades, under long contexts, compaction,
and subagents that never read the file. A PreToolUse hook that denies the push is a device. If
you have been repeating the same correction to an agent, that is the signal to stop writing
instructions and install a device.
## A complete answer covers all five
**The diagnosis is not the answer.** "Instructions are not enforcement" is the right insight,
and it is satisfying to write, but someone asking *"what am I doing wrong?"* has a repo they
need to fix: not a question about their prose. Explaining why the rules fail and stopping
there leaves them exactly where they started. State the insight in a sentence, then spend the
rest of the answer on the replacement.
Replacing an instruction with a device is not one step, it is five, and stopping after the
first leaves the person with a rule that looks enforced and is not. Naming the deny rule is
the easy part and the least of it. Cover every one of these, briefly, before adding depth:
1. **The deny rule, with real syntax.** Show the actual `permissions.deny` entry for their
case, `"Bash(git push --force:*)"`: not a description of one. A pattern they have to
invent themselves is a step where this fails.
2. **A hook where a pattern is not enough.** Deny rules match strings. Anything conditional: a `DELETE` without a `WHERE`, an edit allowed in one directory but not another, a
production hostname, needs a `PreToolUse` hook that inspects the call and returns a deny.
Say which of their two rules needs which.
3. **What the deny message says.** The agent reads it and acts on it, so a bare refusal
produces a workaround, often a worse one. The message must name what was blocked, why, and
what to do instead. This is the one place prose belongs in a device.
4. **Where the config lives, so it applies to everyone.** `.claude/settings.json`, committed.
A rule in `settings.local.json` protects one machine, which is the same failure as
documenting it: the protection exists only where someone remembered to set it up.
5. **Proof that it fires.** Run the blocked action and confirm the denial *and* its message,
then run the legitimate neighbouring action and confirm it still works. Untested hooks fail
open more often than people expect: a regex that does not match the real command string is
a hook that does nothing while looking like protection. **An unverified device is worse
than no device, because it creates confidence without protection.**
Steps 3 and 5 are the ones most often dropped, and they are what separate a device that works
from one that merely exists.
## The three failure modes, and the device for each
**1. The agent does something destructive.** Force-push, `rm -rf`, dropping a table, editing
`.env`, running against production, `git checkout .` over uncommitted work, `--no-verify`.
These are irreversible and fast. Device: **deny at the tool boundary**: a hook or permission
rule that refuses the call before it executes. This is Control and it is the only rung that
matters for irreversible actions.
**2. The agent writes code that looks right and isn't.** Plausible-but-wrong is an agent's
characteristic defect: correct-looking imports of things that don't exist, tests that assert
nothing, error handling that swallows, a stub that returns a hardcoded value. Device: **the
type checker and the test suite as required gates**, plus lint rules against silent failure.
Everything in `guardrails` applies here with extra force, because the volume of
generated code is higher and human review attention per line is lower.
**3. The agent reports success it didn't achieve.** "All tests pass" when the suite wasn't
run; "done" with the build broken. Device: **verification the agent cannot fake**: a Stop
hook that actually runs the tests, or a CI gate. Never accept a claim of completion that only
exists as text.
## Devices, strongest first
### Deny rules in settings.json
The cheapest device and the first thing to install. Permission denies are evaluated before the
tool runs and need no scripting:
```jsonc
{
"permissions": {
"deny": [
"Bash(git push --force:*)",
"Bash(git push -f:*)",
"Bash(git commit --no-verify:*)",
"Read(./.env)",
"Read(./.env.*)",
"Edit(./.env)",
"Edit(./migrations/**)",
"Bash(terraform apply:*)"
]
}
}
```
Reading `.env` matters as much as writing it: an agent that reads a secret can echo it into a
log, a commit, or a message to a third-party service. Deny the read.
A deny entry matches the **start** of the command, so it only holds where the dangerous form
is the prefix. That is why `rm -rf` is not on this list: `"Bash(rm -rf /:*)"` would leave
`rm -fr /`, `rm -Rf /` and `cd / && rm -rf *` untouched while looking like coverage.
Recursive delete needs the hook below, see the `rm` pattern in
`../../assets/devices/claude-hooks/guard_dangerous_commands.py`.
Put team-wide rules in `.claude/settings.json` (committed) and personal ones in
`.claude/settings.local.json` (gitignored), otherwise the rules exist only on the machine of
whoever set them up, which is the same failure as documenting them.
### PreToolUse hooks for anything conditional
When the rule needs logic, "block `DELETE` without a `WHERE`", "block edits to
`schema.prisma` unless a migration exists", "block production hostnames in a connection
string": a hook script inspects the call and returns a deny with a reason.
Templates in `../../assets/devices/claude-hooks/`. The critical detail:
**the deny message is read by the agent and is your only chance to redirect it.** A bare
"denied" produces a workaround attempt, often a creative and worse one. A message that says
what was blocked, why, and what to do instead produces the right action. Write it as you would
write an error message for a colleague:
> Blocked: `DELETE` without a `WHERE` clause on `users`. Unbounded deletes are irreversible
> here. Add a `WHERE` clause, or if a full truncate is genuinely intended, ask the user to
> confirm and run it themselves.
### Stop hooks that verify completion
Run the type check and the test suite when the agent tries to finish. This converts "tests
pass" from a claim into a fact, and it is the single highest-value hook in most repos.
### Machine-checkable CLAUDE.md
Anything in CLAUDE.md that *can* be a check should be one; what remains should be facts the
agent needs rather than rules you hope it follows.
- "Always run `make fmt` before committing" → a pre-commit hook.
- "Never use `any`" → a lint rule with a required check.
- "Don't edit generated files" → a deny rule, plus a header in the generated files.
- "Use `pnpm`, not `npm`" → a deny on `Bash(npm install:*)` with a message naming `pnpm`.
What legitimately stays as prose: architecture, domain vocabulary, where things live, why
past decisions were made. Facts, not commands.
### Make the safe path the easy path
Agents follow the shortest route to a working answer. If `make test` runs the right thing with
the right env, it gets used; if the correct invocation is a fifteen-flag command documented in
a wiki, it does not. Every ergonomic improvement here is a poka-yoke: a `make check` that
bundles fmt + lint + types + tests, a `.env.example` with every key present, a devcontainer or
a single setup script. Ambiguity is where agents improvise, and improvisation is where damage
comes from.
## A caution about over-restriction
Deny rules that block ordinary work produce an agent that spends its turns fighting the
harness, and a user who turns the rules off. Aim the strong devices at **irreversible and
outward-facing** actions, force-push, prod, secrets, destructive SQL, deletion, publishing, and leave ordinary editing and reading alone. Reversibility is the right axis: git makes most
code changes cheap to undo, so they do not need a gate. A rotated credential and a dropped
table do not.
## Verify each device
Same discipline as any other guardrail, and easy to check here: try the blocked action and
confirm the denial and its message, then confirm the legitimate neighbouring action still
works. Untested hooks fail open surprisingly often: a regex that doesn't match the real
command string is a hook that does nothing while looking like protection.
Leave a `poka-yoke:` marker comment on each rule naming what it prevents, and show the user
each config before writing it. Hooks execute code on their machine on every tool call; that is not a change to
make on someone's behalf unseen.
audit8.71 KB
---
name: audit
description: >-
Find footguns in code that already exists: swappable arguments, silent fallbacks, unguarded deletes, signatures that are easy to misuse. Use when someone asks "what could bite us here", "what is easy to misuse", "poka-yoke this repo", or wants a diff or PR reviewed for ways to get it wrong. Ranks by blast radius. For code not yet written use design; for something that already broke use retro.
---
# Poka-Yoke Audit
Find the mistakes that are *available* in this code, then close them. You are not looking for
bugs: a bug is a mistake that already happened. You are looking for **affordances for
mistakes**: places where doing the wrong thing is easy, silent, and looks correct.
The load-bearing question throughout: *if a competent, tired engineer used this at 4pm on a
Friday, what would go wrong and would anything stop them?*
## 1. Establish scope
Default, when the user names no path:
1. `git diff HEAD`: uncommitted work. This is what they are most likely asking about.
2. If the tree is clean, `git diff HEAD~5..HEAD`: recent commits.
3. If neither yields anything (fresh repo, no git), fall back to the risk surfaces below and
say that's what you did.
Widen to the whole repo only when asked ("audit the whole codebase", "full audit"). It is
slow and it buries the important findings in volume. When you do go wide, prioritize by
**risk surface** rather than by directory, go straight to code that touches money,
authentication, authorization, deletion or overwriting, migrations, external I/O,
concurrency, and anything with `admin`, `force`, `bulk`, `sync`, or `delete` in its name.
State the scope you chose in one line before you start, so the user can redirect you cheaply.
## 2. Run the detector, then think
```bash
python3 ../../scripts/detect_hazards.py --diff # path is relative to this SKILL.md
```
Other useful forms: `--paths src/ lib/`, `--staged`, `--since HEAD~10`, `--json`,
`--severity high`, `--id C1 M2` to filter to specific rules. Run `--help` for the full set.
The script finds the mechanically detectable shapes, adjacent same-type parameters, boolean
flag arguments, unbounded deletes, money held as a float, unvalidated request bodies, retries
without an idempotency key. Shapes a real linter already covers, bare `except`, mutable
default arguments, `any` escape hatches, are off by default and named in the footer; `--all`
runs them too. It is a **fast first pass with real false positives**, not an oracle. Treat
each hit as a question to investigate, and read the surrounding code before you believe it.
Then do the part the script cannot: read the interfaces and run the three lenses over them.
**Contact, can the wrong thing fit?** Look at every public signature. Are two adjacent
parameters the same type? Could a caller pass an order ID where a user ID belongs, cents
where dollars belong, a raw string where a validated one belongs? Does the boundary accept
`any` / `dict` / `interface{}` and hope?
**Fixed-value, can an incomplete or wrong-sized set pass?** Is every enum branch handled,
and will adding a variant break the build or silently fall through? Can a bulk operation run
with an empty or unexpectedly huge set? Is config validated as a whole, or discovered
missing at 3am? Are required fields actually required, or optional-with-a-default?
**Motion-step, can the order be wrong?** Must something be called before something else, with
nothing enforcing it? Can a retry double-charge? Can a resource leak on the error path? Can
two callers interleave between a check and the act that depends on it?
The script only sees text. These three questions are where the audit's value comes from.
## 3. Classify every finding
Each finding gets four fields. Fill all four: an unclassified finding is just an opinion.
- **Mistake**: the specific wrong thing a person can do, stated as an action.
*"Call `transfer(dst, src)` with the accounts reversed."*
- **Consequence**: what happens when they do, and how loudly. Silence is the aggravator: a mistake that throws immediately is far less dangerous than one that returns a plausible
wrong answer.
- **Current rung**: what exists today, Control / Warning / Detection / **None**.
- **Proposed device + rung**: the specific change, and the rung it reaches. If you're
proposing Warning, say what would be needed for Control and why you didn't.
## 4. Rank by expected damage, not by count
Priority is **blast radius × ease of mistake**, and nothing else. A hundred stringly-typed
internal helpers matter less than one `delete_users(filter)` where `filter` can be empty.
Blast radius, descending: irreversible data loss or money movement → security or
authorization bypass → silent data corruption → wrong output the user acts on → crash →
degraded experience. A crash ranking *below* silent wrong output is deliberate and worth
saying out loud: loud failures are cheap, quiet ones compound.
Ease of mistake, descending: silent and plausible-looking → requires only forgetting → needs
an unusual-but-reachable input → needs deliberate misuse.
Report the top findings in priority order and stop somewhere sensible, ten well-argued
findings beat forty. Say how many you set aside and why.
## 5. Report
Use this structure. It is short on purpose; the detail lives per-finding.
```markdown
# Poka-Yoke Audit · <scope> · <YYYY-MM-DD>
**Scope**: <what was examined, e.g. "uncommitted diff, 7 files, 340 lines">
**Verdict**: <one sentence, the single most important thing they should fix>
## Findings
### 1. <Short name of the mistake> · <Blast radius>/<Ease>
**Where**: `path/to/file.ts:42`
**Mistake**: <the wrong action a person can take>
**Consequence**: <what happens, and whether it is silent>
**Today**: <Control | Warning | Detection | None>
**Device**: <the specific change> → **<Control | Warning | Detection>**
<a short diff or code sketch>
<if not Control: one line on what Control would cost>
### 2. …
## Set aside
<n low-priority hazards, one line each, or "none">
```
Write it to `docs/poka-yoke/audit-YYYY-MM-DD.md` in the user's repo. If they'd rather not
have a file, keep it in the conversation, ask if it isn't obvious.
## 6. Propose, then apply
Present the findings and wait. Do not edit files yet. These changes alter interface shapes
and ripple through call sites; people reasonably want to see the plan first.
When they approve some or all of it: apply each device, leave a `poka-yoke:` marker comment
at it saying which mistake it blocks, and run the tests.
## Recording what a device is for
Devices only stay valuable if people know they are load-bearing. Without a record, the next
person deletes the "redundant" check or relaxes the "annoying" constraint, and the mistake
comes back. A device that has never fired looks like dead weight precisely because it is
working.
The obvious answer, keep a registry file listing every device, is **wrong, by this skill's
own argument.** A Markdown file someone must remember to update is training, not a device. It
goes stale exactly when it matters: the moment someone removes a constraint without touching
the doc. Do not ask anyone to maintain one.
**Put the reason where the device is.** A marker comment at the constraint travels with it,
gets read by the person about to delete it, and cannot drift out of sync because it is not a
separate thing:
```python
# poka-yoke: rejects a second charge for the same idempotency key [control]
UNIQUE (account_id, idempotency_key)
```
```ts
// poka-yoke: forgetting to await this write would lose it silently [warning]
"@typescript-eslint/no-floating-promises": "error",
```
The bracketed rung is optional. What earns its place is the clause after the colon: the
*mistake*, stated as something a person could do. "Uniqueness constraint" tells a future
engineer nothing; "rejects a second charge for the same key" tells them what breaks if they
drop it.
**If someone wants an index, generate it.** Never hand-maintain it:
```bash
python3 ../../scripts/device_registry.py --write docs/poka-yoke/registry.md
python3 ../../scripts/device_registry.py --check # CI: fails if stale
```
Delete a device and its row disappears; move it and the row follows. That is the difference
between a record that is a device and a record that is a chore.
## Staying useful
The failure mode of this audit is turning into a generic style review. Style findings, naming, formatting, structure, "this could be more readable", do not belong here unless the
unreadability is itself the hazard. If you cannot name a specific wrong action a person could
take, it is not a poka-yoke finding, and including it dilutes the ones that are.
Read `../../references/hazard-catalog.md` for the recurring hazard shapes and their standard
devices, and the matching `../../references/lang-*.md` for what the language can actually
enforce.
authz8.86 KB
---
name: authz
description: >-
Multi-tenant isolation, IDOR and row-level security. Use to find every path where one tenant could read or write another tenant data: "we forgot to filter by org_id", "can users see each other data", "audit these endpoints for cross-tenant leaks", "make an unscoped query impossible". Covers scoped repositories, RLS, default-deny routing and the two-tenant test. For what the UI shows use ux.
---
# Poka-Yoke for Authorization
Cross-tenant data leaks are almost never caused by a wrong access-control decision. They are
caused by *no decision at all*: a query that is correct except it lacks `WHERE tenant_id = ?`,
an endpoint that loads by ID without checking who is asking. The developer did not choose
wrongly; they forgot, in one of the two hundred places the check was required.
That is the signature of a poka-yoke problem: a step that must be performed every single time,
by a human, with nothing enforcing it. The fix is never "be more careful in code review," and
it is never a checklist. **The fix is to make the unscoped query unwritable.**
## Building, not reviewing
Most of the time this mode is reached *while someone is building the thing*, not afterwards.
That changes the deliverable. They asked for the scoping, so produce the scoping, working, complete,
in their stack. Do not hand back a severity table when the person is mid-feature; a list of
findings about code they have not written yet is not useful to them.
Then add a short closing note, three or four lines, covering:
- which misuses the shape you chose makes impossible, and at which rung,
- what you left possible on purpose, and why that tradeoff is the right one here.
That closing note is what stops the device being undone in six months by someone who cannot
see why it is there. It is also the difference between mistake-proofing and a code generator:
the reasoning travels with the code.
When the code already exists and they are asking what is wrong with it, switch to the audit
voice, ranked findings with the mistake, the consequence, and the device. Match the mode to
where they are in the work, not to this file's default.
## The one principle: unsafe should be hard to say
Right now, in most codebases, the unsafe form is the *short* form:
```python
user = db.query(User).filter(User.id == user_id).first() # unscoped: 1 line
user = db.query(User).filter(User.id == user_id,
User.tenant_id == current_tenant).first() # safe: longer
```
Every incentive points at the first line, and it works perfectly in every test, because tests
usually have one tenant. Invert it so the safe form is the default and the unsafe form
requires deliberate, visible effort:
```python
user = tenant_db.users.get(user_id) # tenant scope baked in; cannot be omitted
user = db.unscoped().users.get(user_id) # possible, greppable, reviewable, rare
```
Everything below is a variation on that inversion. When you audit, the question is not "is
this query scoped?" but "**could an unscoped query even be written here?**"
## Devices, strongest first
### 1. Database row-level security (Control, and the one with the widest reach)
RLS enforces the predicate in the database, so it applies to every query from every service,
every migration, every script, and every engineer with a psql shell. It is the only device
that protects you from code paths you did not write. Its reach stops only at roles that are
exempt from policies: superusers, roles with `BYPASSRLS`, and the table owner unless you force
the policy on.
```sql
ALTER TABLE documents ENABLE ROW LEVEL SECURITY;
ALTER TABLE documents FORCE ROW LEVEL SECURITY; -- applies to the table owner too
CREATE POLICY tenant_isolation ON documents
USING (tenant_id = current_setting('app.tenant_id')::uuid);
```
The catch that turns this into a false sense of security: the connection must set
`app.tenant_id` reliably, and a pooled connection that carries a previous request's setting is
a cross-tenant leak with extra steps. Set it per-transaction, and make the middleware that sets
it the only path to a connection. `FORCE ROW LEVEL SECURITY` matters too, without it the
table owner bypasses the policy, and your application user is often the owner.
### 2. Scoped repositories (Control at the type level)
Make the tenant a required constructor argument, so no repository exists without one:
```ts
class DocumentRepo {
// No default. There is no way to construct this without a tenant.
constructor(private readonly db: Db, private readonly tenant: TenantId) {}
async byId(id: DocumentId): Promise<Document | null> {
return this.db.documents.findFirst({ where: { id, tenantId: this.tenant } });
}
}
```
The raw client is then confined to infrastructure code and lint-banned from handlers. The
device is not the `where` clause. It is that the handler has no way to reach a client that
lacks one.
### 3. Authorization in the type (Control)
Rather than loading an object and then checking it, make the check the only way to obtain it:
```ts
// Handlers accept Owned<Document>. There is no path to one that skips the check.
async function authorizeDocument(user: User, id: DocumentId): Promise<Owned<Document>>
```
A handler that takes `Owned<Document>` cannot receive an unauthorized document, so the check
cannot be forgotten: the compiler asks for it. This is the same move as parse-don't-validate,
applied to permission instead of shape.
### 4. Default-deny at the router (Control, cheap)
Require every route to declare its authorization explicitly, and refuse to start if any route
has not:
- A middleware that denies unless a route declares a policy, with a startup check that
enumerates routes and fails the boot on any undeclared one. A new endpoint is then secure
before anyone writes a line of it: the failure mode of forgetting becomes "the service
won't start" rather than "the data is public."
- Public routes are explicitly marked. Making public the opt-in and private the default means
forgetting fails closed.
### 5. Unguessable identifiers (defense in depth, not a device)
UUIDs and ULIDs instead of sequential integers raise the cost of enumeration, and they are
worth using. But an ID is not a permission, anyone who has ever seen the resource still has
the ID forever. Never treat unguessability as the control; it is a mitigation layered behind
one.
## Auditing for missing authorization
The high-yield sequence, in order:
1. **Find every path that loads by ID.** For each: where does the tenant or ownership
constraint come from? If it comes from the request rather than from the session, that is a
finding on its own, `tenant_id` in a request body is client-controlled.
2. **Grep for raw client use in handlers.** Anywhere the unscoped query builder is reachable
from request-handling code is a place the mistake is available.
3. **Check the update and delete paths specifically.** Reads get the attention; writes get
missed, and an unscoped `UPDATE ... WHERE id = ?` lets one tenant modify another's data.
4. **Check every non-primary path**: bulk endpoints, exports, search, webhooks, background
jobs, admin tools, GraphQL resolvers on nested fields, and anything reached via an
association (`document.comments` where the comment scope is assumed rather than enforced).
Nested resolvers are a common blind spot because the parent was checked and the child
inherits nothing.
5. **Check that admin is scoped too.** "Admin" usually means admin *of a tenant*; a global
admin query in a tenant-facing endpoint is a leak.
6. **Ask what happens on a missing session**: does the query run with `tenant_id = None`, and
what does that match? In SQL, `tenant_id = NULL` matches nothing; the dangerous failure is
a query builder that drops a missing predicate and issues the query unscoped.
## The test that proves it
One test pattern is worth more than any number of unit tests here: **create two tenants, then
attempt every operation from tenant A against tenant B's resources, and assert 404 for all of
them.** Table-drive it over your route list so a new endpoint without a case is visible.
Two details matter. Assert **404, not 403**: a 403 confirms the resource exists, which leaks
membership. And make the test enumerate routes automatically where you can, so adding an
endpoint without isolation coverage fails rather than passes silently.
This is a Detection-rung device, and it is the one that tells you whether your Control-rung
devices actually work. Write it even when RLS is in place, especially then, since RLS failures
are silent and total.
## Reporting
Use the finding structure from `audit`. Blast radius for this class is
near-maximum, cross-tenant exposure is a breach, with disclosure obligations, so findings
here outrank almost everything else in an audit. Propose before changing anything, and be
precise about which device reaches Control: adding a `where` clause to one query fixes one
site, and the whole point is that there are two hundred.
data7.1 KB
--- name: data description: >- Pipelines, warehouses, dbt models and metrics, where failure is silently wrong numbers rather than a crash. Use when "the dashboard is wrong", "the numbers do not match", "add data quality checks", "safe backfill", or an upstream schema change broke a join. Covers freshness, row-count and null-rate assertions, data contracts, reconciliation. For a crash rather than wrong numbers use audit. --- # Poka-Yoke for Data Data systems fail differently from application code, and that difference determines every device here. An application bug throws an exception, pages someone, and gets fixed. A data bug produces a number. The number looks fine. Someone makes a decision with it. Three weeks later a person notices revenue looks odd, and now you have three weeks of decisions to unwind and no way to know which were wrong. **In data, silence is the defect.** A pipeline that fails loudly is working correctly. A pipeline that succeeds while producing garbage is the thing to design against, so most devices here are about converting silent wrongness into loud failure, which in Shingo's terms is buying yourself a Warning rung where you currently have nothing at all. ## The four questions Run these over any table or model. They map onto the standard lenses but the data-specific phrasing is what finds things. **Is it there?** *(freshness)*, Did the data arrive at all, and recently enough to be worth trusting? A stale table is the most dangerous artifact in a warehouse because it looks completely healthy. Every table needs a max-age assertion, and dashboards should surface last-updated rather than hiding it. **Is there the right amount?** *(volume, fixed-value lens)*, Row counts against expectation. This catches the breakages that leave every individual row looking fine: a partial load, a filter that silently matched nothing, a join that fanned out 100x. Assert both a floor and a ceiling, and compare against the same weekday historically rather than against yesterday: most business data is weekly-seasonal and a naive day-over-day check will cry wolf every Monday. **Is it shaped right?** *(schema and validity, contact lens)*, Types, nullability, accepted value sets, ranges. Negative quantities, percentages above 100, timestamps in the future, currency codes that don't exist, a `status` value nobody has seen before. **Does it agree?** *(reconciliation)*, Does the warehouse total match the source system? Does the sum of the parts match the whole? This is the only check that catches a logic error the data still looks well-shaped after, everything above validates shape, and a wrong `JOIN` produces perfectly well-shaped, wrong data. It catches what moves a total, not a mis-attribution that nets out. If you install one device, install this one on your revenue-critical tables. ## Devices, strongest first ### Constraints at the write, not tests after it Where the warehouse supports it, `NOT NULL`, `UNIQUE`, `CHECK`, and primary keys are Control: the bad row cannot be written. A dbt test is Detection: the bad row is already in the table and possibly already in a dashboard. Prefer the constraint; use the test where the engine gives you nothing better, which in several columnar warehouses is most of the time, say so explicitly rather than pretending a test is prevention. ### Data contracts at the boundary The most common pipeline break is upstream changing a column without telling anyone. A contract makes that break loud and attributable: - The producer declares the schema, types, nullability, and semantics; changes go through versioning rather than through a surprise. - The consumer validates on ingest and **quarantines** rather than dropping. Silently dropping malformed rows is the data equivalent of `except: pass`: the pipeline goes green while the numbers go wrong. Route bad rows to a dead-letter table with the reason, alert on the rate, and keep them for inspection. - Additive changes are safe; renames and type narrowing are breaking. Treat a rename as a drop plus an add, because that is what downstream experiences. ### Idempotent, resumable loads Every incremental job should be safe to re-run over the same window. Pipelines get retried, by the scheduler, by an on-call engineer, by a backfill, and a non-idempotent load double-counts, which is a silently wrong number of exactly the worst kind. The device: partition-level replace, or `MERGE` on a real business key, rather than blind `INSERT`. Then a re-run converges rather than accumulating. ### Backfills that cannot run away Backfills are the data world's destructive operation. Before running one: - Bound it explicitly: a date range with both ends, never open-ended. - Batch it, with progress recorded, so a failure at 80% resumes rather than restarts. - Write to a staging table and swap atomically, so consumers never see a half-populated table. - Dry-run first, printing the partitions and row counts it will touch. - Know the rollback: if the backfill is wrong, what restores the previous state? If the answer is "nothing", make a snapshot first. That snapshot *is* the device. ### One definition per metric If "active user" is defined in the dashboard, the model, and an analyst's spreadsheet, you have three metrics with one name and they will disagree, usually in a meeting. Define each metric once, in version-controlled code, and have every consumer reference that definition. A metric redefined in a BI tool is a copy that will silently drift. ### Assertions in the pipeline, not beside it The check must be able to **stop the pipeline**, not just report. A test suite that runs after publication and emails a failure lets bad data reach the dashboard, which is the whole problem. Assert between load and publish: build to staging, test staging, promote only on pass. That ordering is the single most valuable structural change in most warehouses, and it costs no new tooling. ## Auditing a pipeline Read the DAG or the model files and work outward from what matters: 1. **Which tables feed decisions or money?** Start there; coverage everywhere is not the goal. 2. **For each: freshness, volume, uniqueness on the key, null rate on required columns, reconciliation to source.** Which exist? Which can actually block publication? 3. **Where are rows silently dropped?** Inner joins that should be left joins, `WHERE` clauses filtering nulls, try/except around row parsing, `on_error='ignore'`. Each is a place the count quietly shrinks. 4. **What happens on re-run?** Trace one job. Does it double-count? 5. **What happens when upstream adds or renames a column?** Break, or silently produce nulls? 6. **Is anything in a dashboard that isn't in version control?** Report with the structure from `audit`, and be explicit about the rung, in data, most devices you can actually install are Warning or Detection, and claiming Control for a dbt test overstates the protection. ## The tone that matters here When numbers have been wrong, the instinct is to find who wrote the bad join. Same rule as everywhere else in this plugin: the finding is that the pipeline could produce a wrong number without anyone noticing. That is a missing assertion, not a missing person.
design7.93 KB
---
name: design
description: >-
Design APIs, schemas, types and state machines so misuse cannot be expressed. Use when writing a new interface and someone asks "what should the types look like", "make invalid states unrepresentable", "so callers cannot screw it up", or wants illegal state transitions rejected. Covers branded types, discriminated unions, typestate, parse-don't-validate. For code that already exists use audit.
---
# Poka-Yoke Design
Mistake-proofing is cheapest before the code exists. Once an interface has callers, every
device you add is a migration; before it has callers, a device is free. So the work here is
front-loaded: decide how this thing will be misused, *then* pick the shape that makes the
misuse unsayable.
This is Shingo's **source inspection**: checking the conditions that produce errors rather
than the errors themselves, and it is the strongest of his three inspection types, because
the error never gets the chance to happen.
## The ritual: enumerate misuse before you write the signature
Before writing an interface, spend real effort on this list. It takes two minutes and it
determines the design.
1. **What are the parameters, and can any two be swapped without complaint?** Same type
adjacent to same type is among the most common footguns in software.
2. **What must a caller remember to do?** Call something first. Call something after. Check a
return value. Close a handle. Pass the right units. Every "must remember" is a defect
scheduled for later.
3. **What states can this thing be in, and which combinations are nonsense?** If you can
construct a value that means nothing, the type is wrong.
4. **What happens on the second call?** Retries, double-clicks, at-least-once queues. If the
answer is "it charges twice," you need a motion-step device.
5. **What's the worst plausible input?** Empty set, enormous set, null, wrong tenant,
yesterday's token, a string from an attacker.
6. **When someone adds a new case next year, what breaks?** The right answer is "the build."
The wrong answer is "nothing, it silently falls through."
Write the answers down where the user can see them, briefly. Then design against them.
## The moves, in preference order
Reach for the highest one the language and situation allow. Each rung down is a real
concession, take it consciously and say why.
### 1. Make the illegal state unrepresentable (Control, contact lens)
The strongest move: change the type so the bad value has no spelling.
- **Distinct types for distinct concepts.** `UserId` and `OrderId` are not both `string`.
Money is not a float. A timeout is not a bare number. Branded types / newtypes / value
objects cost almost nothing and kill an entire class of swap-and-mix-up bugs.
- **Sum types over bags of optionals.** `{status, error?, data?, retryAt?}` permits states
like "succeeded with an error and a retry time." A discriminated union permits exactly the
states that exist. If your struct has N optional fields, it claims 2^N states are legal;
ask how many actually are.
- **Non-empty and bounded collections** when zero or unbounded is nonsense.
### 2. Parse, don't validate (Control at the boundary)
Validation returns a boolean and throws the knowledge away; parsing returns a *new type* that
carries the proof. `validateEmail(s: string): boolean` leaves every downstream function still
holding an unvalidated string. `parseEmail(s: string): Email | Error` means downstream
functions that take `Email` cannot receive garbage: the type system carries the guarantee
for you, forever, for free.
Do this once, at the system's edge: HTTP handlers, queue consumers, config loading, file
parsing, and every third-party response. Inside the boundary, work only with parsed types.
### 3. Make order and lifecycle enforceable (Control, motion-step lens)
When steps must happen in sequence, encode the sequence in types rather than in prose:
- **Typestate**: each operation consumes one state and returns the next, so `.commit()` does
not exist on an uncommitted-and-unvalidated value.
- **Builders that cannot `build()`** until required steps have run, enforced by the type,
not by a runtime check, where the language allows it.
- **Constructors that return ready objects.** If `init()` must be called before use, the
constructor is doing the wrong job. Give it a static factory that does both.
- **Scope-bound resources**: context managers, `defer`, RAII, `using`. Never "remember to close."
- **Idempotency keys as required parameters** for anything that moves money, sends a message,
or mutates external state. Required, not optional: an optional idempotency key is a
suggestion, and suggestions are rung zero.
### 4. Make completeness checkable (Control/Warning, fixed-value lens)
- **Exhaustive matching** with a compiler-enforced never/unreachable arm, so adding an enum
variant breaks the build at every site that must change. This is one of the highest
leverage devices in existence and it costs one line per switch.
- **Required arguments over defaulted ones** when there is no safe default. A default that is
wrong half the time is worse than no default: it hides the decision.
- **Whole-config validation at startup**, so a missing variable fails the deploy rather than
the 3am request.
### 5. Fail fast and loud (Warning)
When the type system genuinely cannot express the constraint, assert at the boundary and
throw. This is a real poka-yoke, one rung down. Make the message name the mistake and the fix.
Two rules that decide whether this rung works at all:
- **No silent fallbacks.** `catch {}`, `except: pass`, `|| defaultValue`, `unwrap_or_default()`
on an error path. These are devices *removed*. They convert a loud mistake into a quiet
one, which is exactly backwards. If a fallback is genuinely correct, the comment must say
which failure it is absorbing and why that failure is expected.
- **Destructive operations default to safe.** Dry-run by default, require an explicit
predicate, refuse to act on an empty or oversized set. `deleteUsers(filter)` with an empty
filter should raise, not truncate the table.
### 6. Where the language can't help, move the device to the data layer
The database is a type system that all your services share. `NOT NULL`, `CHECK`, `UNIQUE`,
foreign keys, and partial unique indexes are Control-rung devices that hold even when someone
writes a script, connects with `psql`, or ships a service in another language. When
application-level enforcement is the only thing standing between you and corrupt data, push
it down.
## Deliver the design with its reasoning attached
You were asked for code, so write the code. But narrate the mistake-proofing in a few lines,
because the reasoning is what stops it being undone later:
- what misuses you enumerated,
- which ones the design now makes impossible, and at which rung,
- which ones you consciously left possible, and why.
That last bullet matters most. Every design leaves something possible; naming it is the
difference between a considered tradeoff and an oversight.
## Restraint
Mistake-proofing has a cost, and past a point it stops paying. Signs you have gone too far:
five wrapper types for one function, a builder for a two-field struct, a type parameter no
caller will ever understand. The test is whether the device prevents a mistake someone would
*plausibly make*, weighted by what happens when they do. An internal helper with two callers
and a trivial failure mode does not need a newtype; a public payments API does.
Sean Goedecke's [critique of the maximalist version](https://www.seangoedecke.com/invalid-states/)
is worth taking seriously: types that model every invariant can become harder to change than
the bugs they prevent. Aim the strongest devices at the highest blast radius, and leave
low-stakes code readable.
Read `../../references/hazard-catalog.md` for the misuse shapes worth
enumerating, and the matching `references/lang-*.md` for what your language can actually
express: the moves above are only as strong as the type system underneath them.
guardrails7.52 KB
--- name: guardrails description: >- Pre-commit hooks, CI gates, lint rules, database constraints and branch protection. Use when a rule needs enforcing rather than documenting: "set up enforcement", "unformatted or untyped code must not get merged", "gate this in CI", "we agreed to X and people still do not", "stop secrets getting committed". Covers baselining and ratcheting so existing violations do not block anyone. For constraining an AI agent use agent-guardrails. --- # Poka-Yoke Guardrails Design-time devices protect the code you are writing now. Guardrails protect the code everyone writes later, including the version of you who is in a hurry. They are Shingo's *successive check*: the next station refuses to accept bad work. The reason this mode exists as its own thing: the most common failure in software quality is agreeing on a rule and then writing it down. A rule in a wiki has a half-life of about one onboarding. The same rule wired into a gate applies itself and costs nothing to remember. ## Building, not reviewing Most of the time this mode is reached *while someone is building the thing*, not afterwards. That changes the deliverable. They asked for the config, so produce the config, working, complete, in their stack. Do not hand back a severity table when the person is mid-feature; a list of findings about code they have not written yet is not useful to them. Then add a short closing note, three or four lines, covering: - which misuses the shape you chose makes impossible, and at which rung, - what you left possible on purpose, and why that tradeoff is the right one here. That closing note is what stops the device being undone in six months by someone who cannot see why it is there. It is also the difference between mistake-proofing and a code generator: the reasoning travels with the code. When the code already exists and they are asking what is wrong with it, switch to the audit voice, ranked findings with the mistake, the consequence, and the device. Match the mode to where they are in the work, not to this file's default. ## Pick the earliest gate that can hold the rule The same rule can live at several points in the lifecycle. Earlier is better, feedback is faster, cheaper, and lands while the author still has the context in their head. But earlier is also easier to bypass. The resolution is to place the device early **and** back it with a gate that cannot be skipped. | Gate | Feedback speed | Bypassable? | Best for | |---|---|---|---| | Type system / compiler | instant | no | anything the types can express, always first choice | | Editor + lint | seconds | yes (ignore comment) | style, banned APIs, unsafe patterns | | Pre-commit hook | seconds | yes (`--no-verify`) | fast checks: secrets, formatting, obvious footguns | | Pre-push hook | ~a minute | yes | medium checks you don't want to wait for on every commit | | CI required check | minutes | **no**, with branch protection | the real enforcement, everything that must not merge | | Database constraint | instant, at write | no | data invariants, across every service and every script | | Runtime assertion | at execution | no | invariants no earlier gate can see | **Never rely on a pre-commit hook alone for anything that matters.** `--no-verify` exists, and people under deadline use it. Use the hook for speed and the CI check for authority; run the same script in both so they cannot drift. ## The devices worth installing Ready-to-adapt templates live in `../../assets/devices/`. Read the relevant one, adapt it to the repo's actual stack, and show the user the file before writing it. - `../../assets/devices/pre-commit/`, `.pre-commit-config.yaml` covering secrets, large files, merge conflict markers, formatting, and a hook for repo-specific rules - `../../assets/devices/github-actions/`: a required-check workflow, plus a migration-safety gate - `../../assets/devices/lint/`: ESLint and Ruff rule sets chosen specifically for mistake-prevention rather than style - `../../assets/devices/claude-hooks/`: Claude Code hooks (see `agent-guardrails`) The rules that pay for themselves in nearly every repo, roughly in order of value: 1. **Secret scanning at commit time.** A leaked key is irreversible; rotation is the only remedy. This is the highest blast-radius mistake a hook can prevent. 2. **Type checking as a required check**, `tsc --noEmit`, `mypy --strict`, `go vet`. This is what makes every design-time device in `design` actually load-bearing. A branded type with no type check in CI is decoration. 3. **The specific lint rules that catch silent failure**: floating promises, unhandled rejections, unchecked errors, bare `except`, empty catch blocks, non-exhaustive switches. Ordinary style rules are not poka-yoke; these are. 4. **Migration safety**: block destructive DDL, or require an explicit acknowledgment for it. Dropping a column in a deploy is a classic irreversible mistake with a trivial device. 5. **Test integrity**: fail CI on `it.only`, `fdescribe`, `@pytest.mark.skip` left behind. A skipped test is a detection device that has been switched off, usually by accident. 6. **Branch protection with required checks.** Without it, none of the above is enforcement. ## Install carefully: a guardrail people hate gets removed This is the mode where a well-intentioned change most easily backfires. A gate that fires constantly on pre-existing code teaches everyone to bypass gates, which is strictly worse than not adding it. Three rules: **Baseline first, then ratchet.** Turning on a strict rule in a large repo yields hundreds of failures and the rule gets reverted by Friday. Instead: enforce on changed files only, or generate a baseline of existing violations and fail only on *new* ones. The violation count can only go down. This is how strictness actually lands. **Be fast or be asynchronous.** A pre-commit hook over about five seconds gets bypassed. Keep commit-time checks to changed files, push the slow work to CI. **Make the failure message teach.** A gate that says `error: rule violated` produces a confused engineer and a workaround. Say what was done, why it is dangerous, and the exact command or edit that fixes it. This is the one place prose belongs in a poka-yoke: at the moment of failure, when someone is guaranteed to read it. Also check what already exists before adding anything. Repos frequently have a lint config or CI workflow that already covers the rule but isn't wired into branch protection, or is set to warn instead of error. Flipping an existing warning to an error is a better change than a new tool. ## Verify the device actually fires An untested guardrail is a guardrail you *believe in*, which is worse than none. It creates confidence without protection. Before you call it done, demonstrate it: 1. Write the mistake it is supposed to catch, deliberately. 2. Run the gate. Confirm it fails, and that the message is the one you wrote. 3. Remove the mistake. Confirm it passes. 4. Show the user both outcomes. Then leave a `poka-yoke:` marker comment on the rule naming the mistake it prevents, see the recording section in `audit`. A device whose purpose nobody remembers is a device that gets deleted during the next cleanup. ## Propose first Show the config files and what they will reject before writing them. Guardrails change how everyone on the team works, and that is not a change to make on someone's behalf without their explicit sign-off, especially the branch-protection and required-check pieces, which you generally cannot apply yourself anyway. For those, hand over the exact settings to click or the `gh api` command to run.
llm9.38 KB
---
name: llm
description: >-
AI features you ship to users: structured output, tool schemas, prompt injection, evals. Use when "the model returns bad JSON", "it hallucinates", "stop it calling the wrong tool", "add evals", or an LLM feature can trigger refunds, emails or writes. Covers schema-constrained output, idempotent tool calls, confirmation gates. For agents editing your repo use agent-guardrails.
---
# Poka-Yoke for LLM Features
This is about AI features **you ship to users**: not about agents editing your repo, which is
`agent-guardrails`.
The defining property of an LLM is that it is a component with a non-zero error rate on every
call, and no amount of prompt engineering drives that to zero. This is not a defect to fix; it
is the material you are building with. Shingo's framing fits perfectly: you do not make the
operator more careful, you build the jig.
Which means the central discipline here: **prompt instructions are rung zero.** "Always respond
with valid JSON," "never make up a citation," "do not reveal the system prompt". These are
requests to an unreliable component, and they are the LLM equivalent of a comment saying "be
careful." They help, they are worth writing, and they are not devices. A device is something
outside the model that constrains what it can produce or what its output can reach.
## Building, not reviewing
Most of the time this mode is reached *while someone is building the thing*, not afterwards.
That changes the deliverable. They asked for the feature, so produce the feature, working, complete,
in their stack. Do not hand back a severity table when the person is mid-feature; a list of
findings about code they have not written yet is not useful to them.
Then add a short closing note, three or four lines, covering:
- which misuses the shape you chose makes impossible, and at which rung,
- what you left possible on purpose, and why that tradeoff is the right one here.
That closing note is what stops the device being undone in six months by someone who cannot
see why it is there. It is also the difference between mistake-proofing and a code generator:
the reasoning travels with the code.
When the code already exists and they are asking what is wrong with it, switch to the audit
voice, ranked findings with the mistake, the consequence, and the device. Match the mode to
where they are in the work, not to this file's default.
## The boundary: nothing the model says is trusted until something checks it
Draw the same line you would draw around any external, untrusted input, because that is
exactly what model output is, and doubly so when the model has read user-supplied text.
### Structured output over prose parsing (Control, contact lens)
Never regex a model's prose. Use the provider's constrained/structured output mode with a
schema, then validate the parsed result against that schema yourself:
```python
class Extraction(BaseModel):
model_config = ConfigDict(extra="forbid")
sentiment: Literal["positive", "neutral", "negative"]
confidence: float = Field(ge=0.0, le=1.0)
```
Constrained decoding makes malformed output largely unrepresentable, and the schema check
catches the rest. That removes the whole class of parse failures, malformed JSON, missing
fields, invented enum values.
Two things the schema still cannot tell you: whether the values are *correct*, and what to do
when validation fails. Decide the failure path explicitly, retry once with the error fed
back, then fall back to a deterministic path or return a clear failure. A silent default here
is `except: pass` with a language model attached.
### Enumerate rather than generate wherever possible
The strongest device in this whole mode: if the output is a choice from a known set, have the
model choose an ID from a list you supply and reject anything not in it. A model asked to
produce a category name will invent one eventually; a model choosing among five IDs cannot.
Applies to routing, classification, tool selection, and picking a record, and it converts an
open-ended generation problem into a closed-set one that a `Literal` type enforces.
### Ground factual claims, and make ungrounded output impossible to render
For anything retrieval-backed, require the response to cite retrieved chunk IDs, then verify
each cited ID actually exists in what you retrieved and drop or flag claims that don't
resolve. That check establishes that a citation resolves, not that the chunk it points at
supports the claim, where the claim is consequential, add an entailment check or human review
on top. Prompting for citations is rung zero; *verifying* them is a real device. Show the
source in the UI so the user can check. This is the interface half of the same device.
When retrieval returns nothing relevant, the correct behavior is to say so. A model handed no
context will answer anyway, and that answer is invention. Check for the empty-context case in
code, before the call, and short-circuit.
## Side effects: the model proposes, the system disposes
The most expensive LLM bugs are not wrong text. They are actions. Refunds issued, emails sent,
records deleted, all because a model decided to.
- **Split tool calls by reversibility.** Read-only tools execute freely. Anything irreversible
or outward-facing, payment, email, deletion, publishing, external writes, requires a human
confirmation that names the specific action and its parameters. This is the same ladder as
everywhere else; irreversible actions need Control.
- **Make the tool schema tight.** Enums instead of free strings, required parameters instead of
optional ones, ranges on numbers, and no "extra context" free-text field the model can use
to smuggle in intent. A wide tool schema is a wide attack surface and a wide mistake surface.
- **Validate arguments server-side, always.** The model is a client, and a client's input is
never trusted. `refund(amount)` must re-check the amount against the actual order: the
model saying `9999` is not authorization.
- **Idempotency keys on every effectful tool call**, backed by a unique constraint. Agent loops
retry; retries double-charge. This is hazard M2 with a higher retry rate than any human path.
- **Scope credentials to the user, not to the service.** If the tool runs with service-level
access, a prompt injection reaches everything. Pass the requesting user's authorization
through, so the model cannot exceed what that user could do, see `authz`.
## Prompt injection is a boundary problem, not a prompt problem
Any text the model reads, user input, retrieved documents, web pages, emails, tool results, can carry instructions. No system prompt reliably prevents this, and treating it as a prompt
engineering problem is why it keeps happening.
The devices are structural: keep untrusted content clearly delimited and labeled as data;
never let model output flow into a privileged action without validation or confirmation;
scope permissions so a successful injection has a small blast radius; and treat any model
output that will be rendered as HTML, executed as SQL, or passed to a shell exactly as you
would treat user input from an attacker, because functionally it is.
The load-bearing question is not "can the model be tricked?" (yes) but "**what can the model
reach if it is tricked?**"
## Bounds: cost and loops
An agent loop with no cap is an unbounded resource operation, hazard F7 with a billing
account attached. Set a maximum step count, a token budget per request, and a wall-clock
timeout, all enforced in your code rather than requested in the prompt. Alert on cost per
user, and cap it per tenant so one runaway conversation cannot become a five-figure invoice.
## Evals are the detection rung, and they are load-bearing
You cannot unit-test a probabilistic component, but you can measure it, and without
measurement you have no idea whether a prompt change helped.
- **A held-out eval set with assertions**, run in CI on every prompt, model, or retrieval
change. Prompts are code with no type checker. This is the only gate they have.
- **Assert on the structured fields**, which are checkable, rather than on prose similarity.
This is another reason structured output pays for itself.
- **Every production failure becomes an eval case.** This is the `retro` loop applied
to a component that cannot be fixed, only constrained: you cannot patch the model, so the
regression test *is* the fix, and it must cover the class rather than the one input.
- **Pin the model version.** A provider updating a model underneath you is an unannounced
deploy of your most unpredictable component. Pin it, and re-run evals before moving.
## Auditing an LLM feature
1. **Where does model output go?** Trace each path. Which reach a database, an API, a shell,
the DOM, or a user as fact? Each needs a check at that boundary.
2. **What is parsed from prose that could be structured?**
3. **Which tools have irreversible effects, and what gates them?**
4. **What untrusted text enters the context, and what could an instruction in it reach?**
5. **What happens when the model fails**: malformed output, refusal, timeout, rate limit,
empty retrieval? Is there a deterministic fallback, or does it fail silently?
6. **What bounds exist on steps, tokens, and cost?**
7. **Is there an eval suite, does CI run it, and does a regression block the merge?**
Report with the structure from `audit`, and be honest about rungs, with a
probabilistic component, most in-model devices are Warning at best, and only the checks
*outside* the model reach Control.
ops9.91 KB
--- name: ops description: >- Deploys, schema migrations, rollback and infrastructure. Use when "can I ship this on Friday", "this migration is scary", "what is the blast radius", "prevent accidental deletion of the database", or a change drops a column. Covers expand/contract, canary rollout, kill switches, prevent_destroy, tested backups. For an incident that already happened use retro. --- # Poka-Yoke for Deploys and Infrastructure Operations is where irreversible mistakes concentrate. Code mistakes are usually recoverable, git remembers, a revert ships in twenty minutes. A dropped table, a deleted bucket, a rotated credential, or a terminated stateful node is not recoverable by any amount of engineering after the fact. So the governing question in this mode is different from the rest of the plugin. Not "can this be done wrong?" but: **when this is done wrong, how much is affected, and can it be undone?** Those two axes, blast radius and reversibility, determine every device below. ## Answer these four first Before any framework or table, establish these. They are what an operator actually needs, and they are the things most often left out: an answer that skips them is not useful no matter how well organized the rest is. Say each one plainly, in a sentence, before going deeper. 1. **What here is irreversible, and what restores it?** Name the specific unrecoverable step: a dropped column, a deleted bucket, a rotated key. Then say what would restore it: a backup, a snapshot, a rebuild. **If the answer is "nothing", say so explicitly.** An irreversible step with no stated restore path is the single most important thing you can tell someone, and it is the first thing to get lost in a longer answer. 2. **What breaks during the rollout window?** Deploys are not atomic. For a period, old code runs against the new state. Say what happens in that window, usually this is the actual outage, not the change itself. 3. **Can the irreversible part ship separately?** Most changes are a reversible part and an irreversible part stapled together. Splitting them is nearly always available and nearly always right; say so concretely rather than in general. 4. **If it goes wrong, who is available and how fast is rollback?** Timing questions are about staffing and recovery speed, not superstition. A change that reverts in two minutes is fine on a Friday afternoon; one that needs a four-hour restore with two people asleep is not. Cover all four even when the answer is brief. If you only have room for a little, spend it here rather than on the taxonomy: the rungs below are how to *think* about the fix, but these four are what the person has to know before they ship. ## The blast radius ladder Most ops poka-yoke is not about preventing the bad change. It is about ensuring the bad change reaches 1% of traffic instead of 100%. You cannot prevent every bad deploy; you can make bad deploys cheap. | Rung | Device | What it buys | |---|---|---| | **1 Control** | The dangerous operation is impossible in this environment, `prevent_destroy` on stateful resources, deletion protection on the database, no human write access to prod, immutable infrastructure | The mistake cannot be made at all | | **1 Control** | Progressive rollout with automatic rollback on error-rate: the bad version is withdrawn before most users see it | The mistake is capped and self-healing | | **2 Warning** | Required plan review, a deploy that prints what it will destroy and demands typed confirmation, alerts wired to the rollout | The mistake is visible at the moment of decision | | **3 Detection** | Post-deploy smoke tests, monitoring, an on-call human | The mistake is found after users find it | | **0** | A runbook step that says "double-check the environment first" | Nothing | ## Reversibility is the highest-leverage property Before adding any gate, ask whether the operation can be made reversible instead: a reversible operation needs far weaker devices, because the cost of the mistake collapses. - **Soft delete and retention windows** on anything user-facing. S3 versioning plus MFA delete, database point-in-time recovery, trash with a 30-day window. - **Deletion protection flags** on databases, buckets, clusters, and load balancers. These cost nothing and stop the single most expensive class of cloud mistake. - **Backups that have actually been restored.** An untested backup is a belief, not a device, and this is the most common false sense of protection in the industry. Restore drills on a schedule, timed, into a real environment. If nobody has restored it, treat the data as unbacked when you assess blast radius. - **Immutable artifacts** so rolling back means redeploying a known-good image, not rebuilding and hoping the build is reproducible. ## Schema migrations: expand and contract Co-deploying a destructive schema change with the code that depends on it is an outage, not a risk, during the rollout window old code necessarily runs against the new schema. The pattern, one deploy per step: 1. **Expand**: add the new column/table, nullable, with no code depending on it. 2. **Backfill**: in batches, resumable, throttled, with progress recorded so a failure resumes rather than restarts. 3. **Dual-write**: new code writes both old and new; both remain readable. 4. **Switch reads**: behind a flag, so switching back is instant. 5. **Contract**: drop the old column, in a later deploy, once nothing references it. Steps 1–4 are reversible: the old column stays readable throughout, so rolling the deploy back is enough. Only step 5 is not, which is exactly why it gets its own deploy and its own gate. The device that makes this stick is a CI check that refuses any `DROP`, `TRUNCATE`, or `ALTER ... DROP` in a changed migration unless the PR carries an explicit approval label, see `guardrails` for the gate itself. Migration-specific hazards worth checking every time: a lock taken on a large table during peak traffic; a backfill with no batch limit; an index created without `CONCURRENTLY`; a `NOT NULL` added without a default on a populated table; a rename, which is a drop and an add wearing a disguise. ## Feature flags and kill switches A kill switch is a poka-yoke for a change you cannot fully test in advance. It converts "roll back a deploy" (minutes, and impossible if the migration already ran) into "flip a boolean" (seconds). Ship risky changes dark, behind a flag, then enable progressively. Two things make flags devices rather than debt: - **The off path must be tested**, not just the on path. A kill switch whose disabled branch was never exercised is a second untested code path shipped at your worst moment. - **Flags need an expiry.** A permanent flag is a permanent untested branch and a permanent source of "works for some users only" bugs. Track age and remove them; stale flags are the standard way this device turns into a hazard. ## Infrastructure as code - **`prevent_destroy` on every stateful resource**: databases, buckets, volumes, DNS zones. One line, and it turns the worst cloud accident into a failed plan. It is Control against the accident, not against intent: removing the block is another one-line change, so review has to read a diff that deletes a `prevent_destroy` as itself a destructive change. - **Plan review as a required check**, with the plan output posted to the PR. A human approving a diff they cannot see is rung zero. - **Fail the plan on unexpected destruction**: a check that counts destroy actions and blocks the apply unless the change is explicitly labeled as intentionally destructive. Terraform will happily replace a database to change one immutable attribute, and the plan says so in a line people skim past. - **Separate state and credentials per environment**, so a misconfigured shell cannot point a staging apply at production. Environment confusion is a mistake of *context*, and the device is making the contexts physically incapable of touching each other. - **No console access for routine work.** Manual changes drift from code and are invisible to review; drift detection turns that into a Warning at minimum. ## Production access The strongest device is not needing access: good observability, safe read-only debugging tools, and self-service runbooks remove most reasons a human ever holds a prod shell. Where access is genuinely needed: time-boxed and audited, read-only by default, write access requiring a second approver, and a shell prompt that makes the environment impossible to mistake. Environment confusion, running the staging command against prod, is a top-tier ops mistake and it is fixed by making prod look and feel different, not by remembering. Wrap dangerous scripts so the safe form is the easy one: dry-run by default with `--apply` to commit, print the affected count before acting, refuse to run against prod without an explicit flag, and refuse an empty or wildcard target. ## Auditing an ops setup Work through these, and report using the finding structure from `audit`: 1. **What is irreversible today?** Every resource whose loss is unrecoverable. Which have deletion protection? When was the backup last *restored*, not last taken? 2. **What is the blast radius of a bad deploy?** All users at once, or 1%? Is rollback automatic on an error-rate signal, or does it need a human who is asleep? 3. **Can a migration and its dependent code land together?** Is anything stopping it? 4. **Can a staging command reach production?** Shared credentials, shared state, an ambiguous prompt, a `--env` flag defaulting to prod. 5. **What has no kill switch?** Anything risky that can only be withdrawn by a full deploy. 6. **Which flags are older than 90 days?** Propose devices before applying them, and never apply infrastructure changes without explicit approval: an `apply` is exactly the class of outward-facing, hard-to-reverse action that belongs to the user, not to you. For anything you cannot run yourself (console settings, branch protection, IAM), hand over the exact steps or CLI command.
poka-yoke11.9 KB
---
name: poka-yoke
description: >-
Mistake-proofing for anything: code, config, a schema, a process, a runbook, a form. Use when someone says "poka-yoke this", "mistake-proof it", "make this harder to get wrong", or runs /poka-yoke with no mode named. Applies Shigeo Shingo's method directly to any subject, and hands off to a specialist mode when one clearly fits. Start here when no other poka-yoke mode obviously applies.
---
# Poka-Yoke: Mistake-Proofing for Software
Shigeo Shingo's insight, from the Toyota Production System: **people will always make
mistakes; that is not the problem worth solving. The problem is letting a mistake become a
defect.** So you stop trying to make humans more careful and start redesigning the work so
the mistake either cannot physically happen or announces itself immediately.
A poka-yoke ("poh-kah yoh-kay", ポカヨケ) is a *device*: a jig, a shape, a counter: not
an instruction. In software: a type, a constraint, a hook, a schema, a state machine. The
single most important consequence:
> **A comment, a docstring, a wiki page, a code review checklist, or a line in CLAUDE.md
> saying "don't do X" is not a poka-yoke.** It is training. Training degrades. A device
> does not. If your proposed fix relies on someone remembering something, keep going.
## The two axes
Every real poka-yoke answers two questions. Use both when you classify a hazard or propose a
device. They are the difference between this method and generic code review.
### Axis 1, Regulatory function: what happens when the mistake occurs?
This is a strict preference ladder. Always reach for the highest rung you can afford.
| Rung | Name | What it does | Software examples |
|---|---|---|---|
| **1** | **Control** | The mistake is **impossible**. The work cannot proceed. | Type won't compile · `NOT NULL` / `CHECK` / unique constraint · required function argument · private constructor + smart constructor · PreToolUse hook returns deny · protected branch |
| **2** | **Warning** | The mistake is possible but **announced at the moment it happens**. | Lint error in the editor · failing CI gate · runtime assertion that throws · confirmation prompt naming the exact thing being destroyed |
| **3** | **Detection** | The mistake ships, and something **finds it afterward**. | Tests · monitoring · alerting · reconciliation job |
| **0** | *(not a poka-yoke)* | Relies on a human remembering. | Docs · comments · training · "be careful" · review checklists |
Shingo's rule: prefer **control** over **warning**, always, and only settle for warning when
control is genuinely too expensive, then say *why* out loud. In software the honest reason
is usually "the language can't express it" or "it would break every existing caller," and
both are worth stating explicitly so the tradeoff is visible.
### Axis 2, Setting function: how does the device notice?
Shingo's three detection methods map cleanly onto software. These are your **inspection
lenses**, run all three over any interface and you will find hazards that a general
code review misses.
| Method | Factory floor | The question to ask code | Software devices |
|---|---|---|---|
| **Contact** | The part physically won't seat unless it's the right shape and orientation | **Can the wrong thing fit?** | Distinct types instead of shared primitives · branded/newtype IDs · parse-don't-validate at boundaries · units in the type · discriminated unions instead of bags of optionals |
| **Fixed-value** | A counter says all 6 screws were fitted | **Can the wrong count or an incomplete set pass?** | Exhaustive `match`/`switch` over an enum · required fields · "all migrations applied" check · row-count guard on a bulk write · checksums · config validated as a whole at boot |
| **Motion-step** | A sensor confirms step 3 happened before step 4 | **Can the steps happen in the wrong order, or be skipped?** | Typestate · builder that cannot `.build()` until required steps run · state machines with illegal transitions unrepresentable · idempotency keys · RAII / `defer` / context managers · transactions |
### The third principle: inspect at the source
Shingo separated **source inspection** from **informative inspection**, which finds the defect
only after it exists and comes in two forms. Ranked best first, that is three places you can
put the device.
1. **Source inspection**: check the *conditions* before the error can occur. Designed in
where you can, enforced at runtime where you cannot.
The type, the constraint, the signature.
2. **Self-check** (informative): the work checks itself as it happens. Runtime. Assertions,
fail-fast, validation at the boundary.
3. **Successive check** (informative): the next station checks the previous one. Review, CI,
QA.
Push every device as far up this list as it will go. A CI gate that catches a bad migration
is good; a schema that makes the bad migration unwritable is better and costs less forever.
## How to use this skill
You have been asked to apply the method. There are two ways to do that, and picking the
wrong one wastes the request.
**If a specialist mode clearly fits the subject, load it**: the table below maps them. Those
files carry the domain detail this one does not: what a tenant-scoping device looks like, what
expand/contract means, which lint rules catch silent failure.
**If none clearly fits, apply the method here.** This file is self-contained enough to do
that, and refusing to act because the subject is not on a list would be its own failure. A
Terraform module, a support runbook, a spreadsheet everyone edits, a release checklist, a
prompt template, an onboarding process, a physical workflow: the method works on any of them,
because Shingo developed it on an assembly line, for people fitting springs into switches, and
not for software at all.
Applying it directly means four steps, in order:
1. **Name what is being done, and by whom.** A device protects a specific action taken by a
specific person or system. "The pipeline" is not an action; "an engineer re-runs the deploy
job after it fails halfway" is.
2. **Run the three lenses** over that action, can the wrong thing fit, can an incomplete or
wrong-sized set pass, can the steps happen in the wrong order. Most subjects yield
something on at least one.
3. **For each hazard found, state it as a mistake someone could make**, what happens when they
do, whether it is silent, and what exists today to stop it.
4. **Propose the highest-rung device you can afford**, and say which rung it reaches. If you
land on Warning, say what Control would have required and why you did not take it.
Then apply the two rules in *How to talk about this* below: name the mistake rather than the
mistaken, and never let the answer come out as "be more careful" or "document it". Those are
rung zero, and the whole method exists because they do not work.
**If the request is bare**, `/poka-yoke` with nothing attached, look at what is actually in
front of you: the current diff, the file under discussion, the thing the conversation has been
about. Say what you picked in one line before starting, so it is cheap to redirect you. If
there is genuinely no subject, ask what they want mistake-proofed rather than guessing.
## Choosing a mode
These are specialist modes, each carrying the full working method for its domain. Load one
when it clearly fits. When two apply, do them in the order listed. When none does, apply the
method directly as described above: the table is a shortcut, not a gate.
| If they are… | Use | Typical asks |
|---|---|---|
| Looking at code that already exists and asking what could go wrong | **`audit`** | "poka-yoke this repo" · "what's easy to misuse here" · "find the footguns" · "review this PR for ways to screw it up" |
| About to write an API, module, schema, or data model | **`design`** | "design this so it can't be misused" · "make invalid states unrepresentable" · "what should this signature be" |
| Wanting mechanical enforcement in the pipeline | **`guardrails`** | "add pre-commit hooks" · "gate this in CI" · "enforce it so it can't be merged" · "lint rule for this" |
| Dealing with something that already broke | **`retro`** | "this happened again" · "postmortem" · "make sure this never recurs" · "why did this get through" |
| Building or reviewing a user interface | **`ux`** | "users keep deleting the wrong thing" · "make this form harder to get wrong" · "add a confirmation" · "this flow is error-prone" |
| Deploying, migrating, or changing infrastructure | **`ops`** | "deploy this safely" · "what's the blast radius" · "this migration is scary" · "add a kill switch" · "prevent accidental deletion" |
| Working on pipelines, warehouses, or metrics | **`data`** | "the numbers are wrong" · "add data quality checks" · "safe backfill" · "upstream changed the schema" |
| Working on multi-tenant, permissions, or endpoints | **`authz`** | "can users see each other's data" · "IDOR" · "tenant isolation" · "we forgot to filter by org_id" |
| Shipping an AI feature to users | **`llm`** | "the model returns bad JSON" · "it hallucinates" · "prompt injection" · "add evals" |
| Worried about what an AI agent will do to the repo | **`agent-guardrails`** | "stop Claude from touching prod" · "hooks so the agent can't break X" · "make this repo safe for agents" |
Two of these are easy to confuse. **`llm`** is for AI features *you ship to users*;
**`agent-guardrails`** is for constraining an agent that *works on your repo*.
Modes compose, and real requests often need two. An incident involving a bad migration is
`retro` for the analysis and `ops` for the device. A cross-tenant leak is
`retro` plus `authz`. Read both; the retro decides *what* to install and
the domain skill decides *which device*.
If the request is a general question ("what is poka-yoke", "how does this apply to software")
answer from this file directly: the two axes above are the substance.
## The hazard catalog and language specifics
The recurring ergonomic hazards: the signatures and shapes that reliably produce mistakes, live in `../../references/hazard-catalog.md`, each with the lens that finds
it and the device that fixes it. Read it when you are auditing or designing; it is the
working vocabulary for both.
Language-specific devices (what the type system will and won't let you express, which
constructs are idiomatic, what the linters can enforce) live in:
- `../../references/lang-typescript.md`
- `../../references/lang-python.md`
- `../../references/lang-rust-go.md`
Read only the file for the language in front of you. If the language isn't covered, the two
axes still apply, work out which construct in that language gives you contact, fixed-value,
and motion-step checking, and say which rung you landed on.
## How to talk about this
Two habits keep the analysis honest and keep people from getting defensive:
**Name the mistake, not the mistaken.** "This signature lets a caller swap the two IDs" is
actionable and true. "The developer should have been more careful" is neither. Shingo was
emphatic that blaming the operator is how organizations avoid fixing the process. Write
findings about the code's affordances, never about who wrote it.
**Say which rung you achieved, and what stopped you going higher.** A recommendation that
reads "added a runtime assertion (warning), control would need a newtype, which touches 40
call sites" gives the reader a real decision. One that reads "added validation" does not.
## Applying changes
Propose before you edit. Show the hazard, the proposed device, and the rung it reaches, then
wait for a go-ahead before changing files: the whole point of this method is that it changes
the shape of an interface, and that is precisely the kind of change people want to see first.
Once approved, apply it and leave a marker comment saying what mistake it prevents (see the
recording section in `audit`).
The exception is when someone has explicitly asked you to write new code, in `design`
mode, mistake-proofing *is* the code they asked for, so build it, then narrate which hazards
you designed out and why.
retro7.12 KB
--- name: retro description: >- Turn a bug, outage or repeated mistake into a device that makes the whole class impossible. Use when something already broke: "make sure this never happens again", "this is the third time", "postmortem", "how did this get through". Root-causes to the missing constraint, then sweeps every other site where the mistake is still available. For a pipeline use data, a deploy use ops, cross-tenant use authz, an AI feature use llm. --- # Poka-Yoke Retro A defect got out. The fix for the defect is the easy part and is usually already done or obvious. This mode is about the harder and more valuable question: **what made the mistake available, and what device removes it for good?** Shingo's framing is the whole method here. Do not ask why the person erred, people err, that is a constant. Ask why the *process permitted* the error to become a defect, and what would have physically stopped it. ## 1. Separate the three things People conflate these, and conflating them is why incidents repeat. - **The defect**: what the user or system experienced. "Customers were charged twice." - **The mistake**: the specific human action that produced it. "The retry path called `charge()` again without an idempotency key." - **The hazard**: the property of the system that made that mistake possible and silent. "`charge()` accepts an optional idempotency key and succeeds without one." Fixing the defect ships today. Fixing the mistake helps one code path. **Only fixing the hazard prevents recurrence**, and the hazard is almost always a missing constraint, not a missing piece of knowledge. Write all three out explicitly before proposing anything. If you cannot state the hazard as a property of the system, you have not found it yet. ## 2. Ask why until you reach a constraint Run the whys, with one discipline: **an acceptable terminal answer is a missing constraint, never a missing human quality.** If a chain ends in "they forgot," "they didn't know," "they were rushing," or "it wasn't documented," you stopped one step early, keep going and ask why forgetting was possible, why the knowledge was needed at all, why the system accepted the result. > Double charge → retry called `charge()` twice → the retry path didn't pass an idempotency > key → **the key is an optional parameter** → *why is it optional?* → it was added later and > made optional to avoid breaking callers → **there is no compile-time or database-level > requirement that a charge be idempotent.** That last line is the hazard, and it is fixable: make the parameter required, or add a unique constraint on `(account_id, idempotency_key)`. Compare it to "the engineer should have passed the key," which is fixable only by hiring different humans. Also ask the escape question separately: **what should have caught this and didn't?** Usually there was a device: a test, a review, a type, and it was absent, disabled, or too weak. That gap is a second finding in its own right. ## 3. Sweep for the class. This is the step that gets skipped A poka-yoke that fixes one call site is not a poka-yoke. Before proposing anything, find **every other place the same mistake is still available.** This is almost always where the real value of a retro sits, and it is the step people omit under time pressure. Search by the shape of the hazard, not by the text of the bug: - Every other caller of the same function or endpoint. - Every other function with the same dangerous signature shape, other optional-when-it-should- be-required parameters, other same-type adjacent arguments, other unguarded bulk operations. - The same pattern in sibling services, other languages in the monorepo, scripts, jobs, and infrastructure code. - Run `python3 ../../scripts/detect_hazards.py --paths <repo> --id <hazard-id>`: the ID is the one printed with each finding, to catch instances you would not have thought to grep for. Report the count plainly: *"the same hazard exists at 6 other call sites"* changes the conversation about how much the fix is worth. ## 4. Choose the device by rung Now propose, using the ladder from the router skill. For an incident that already cost something real, push hard for **Control**: you have the strongest evidence you will ever have that this mistake happens. | Rung | For this incident, that would mean | |---|---| | **Control** | Required parameter · database unique constraint · type that cannot represent the bad state · CI check that cannot be merged past | | **Warning** | Lint rule · runtime assertion · alert at the moment of the action | | **Detection** | Regression test · monitor · reconciliation job | | **None** | "Added a note to the runbook" · "reminded the team" · "added a review checklist item" | A regression test is genuinely valuable and you should write one. It proves the fix and stops this exact path regressing. But be honest that it is rung 3: it catches the mistake after someone makes it, and only on the path you thought of. If the retro produces *only* a test, say so, and say what a Control-rung device would have required. Beware the fix that is really rung zero wearing a costume: more documentation, a new checklist item, a Slack reminder, a training session, an extra required reviewer. These feel like action and change nothing. If that is genuinely all that is possible, name it as an accepted risk rather than a resolution. ## 5. Write it up ```markdown # Retro · <short title> · <YYYY-MM-DD> **Defect**: <what was experienced, with blast radius: how many, how much, how long> **Mistake**: <the specific action taken> **Hazard**: <the system property that made it possible and quiet> ## Why it was possible <the chain, ending at a missing constraint> ## Why nothing caught it <the device that should have existed, was disabled, or was too weak> ## Class sweep <n other sites where this mistake is still available, list them> ## Devices | Device | Rung | Covers | Status | |---|---|---|---| | <change> | Control | all N sites | proposed | | <regression test> | Detection | the original path | done | ## Accepted risk <what remains possible, and why that is acceptable> ``` Save to `docs/poka-yoke/retro-YYYY-MM-DD-<slug>.md`, and put a `poka-yoke:` marker comment at each installed device naming the mistake it prevents. That is what stops a future engineer removing it as dead weight, since by then it will never have fired. See the recording section in `audit`; do not ask anyone to hand-maintain a registry file. ## 6. Verify the device before you close it Prove the fix. Reproduce the original mistake against the new device and show it being refused, then show the correct path still working. A device that was never observed to fire is a belief, not a control, and after an incident, a false sense of protection is the most expensive thing you can ship. ## Tone Write about the system, never the person. Not because it is polite, but because it is more accurate and it is the only version that produces a fix: "the engineer should have been more careful" has no implementation. Shingo's argument was that blaming the operator is precisely how organizations avoid improving the process. Names belong in the timeline if at all; the analysis is about affordances.
ux10.1 KB
--- name: ux description: >- Forms, destructive actions and flows users get wrong. Use when "users keep deleting the wrong thing", "add a confirmation dialog", "this flow is error-prone", or building a delete, bulk action, checkout or settings page. Covers undo over confirmation, type-to-confirm, safe defaults, input constraints, double-submit. For the server-side rules behind the screen use authz. --- # Poka-Yoke for Interfaces Shingo built jigs so an assembly worker could not seat a part backwards. A form is a jig. The same ladder applies, and the design literature arrived at the same place independently, Don Norman's *forcing functions* and Nielsen's *error prevention* heuristic describe the same move from a different tradition. The single reframing that does most of the work here: **an error message is a failure of the design, not a feature of it.** If your interface can tell the user they did something wrong, it usually could have stopped them doing it. Validation that fires after submission is rung 3. An input that cannot hold the wrong value is rung 1. ## Building, not reviewing Most of the time this mode is reached *while someone is building the thing*, not afterwards. That changes the deliverable. They asked for the interface, so produce the interface, working, complete, in their stack. Do not hand back a severity table when the person is mid-feature; a list of findings about code they have not written yet is not useful to them. Then add a short closing note, three or four lines, covering: - which misuses the shape you chose makes impossible, and at which rung, - what you left possible on purpose, and why that tradeoff is the right one here. That closing note is what stops the device being undone in six months by someone who cannot see why it is there. It is also the difference between mistake-proofing and a code generator: the reasoning travels with the code. When the code already exists and they are asking what is wrong with it, switch to the audit voice, ranked findings with the mistake, the consequence, and the device. Match the mode to where they are in the work, not to this file's default. ## The ladder, applied to interfaces | Rung | In a UI | Example | |---|---|---| | **1 Control** | The wrong action cannot be taken | Date picker that excludes unavailable dates · quantity capped at stock · Submit that does not exist until the form is valid · destructive action absent for users without permission | | **2 Warning** | Possible, but flagged at the moment it happens | Inline field validation on blur · a live character counter turning red · a banner warning that this will affect 4,312 users | | **3 Detection** | Caught after submission | Error summary at the top of the page · server rejects it · support ticket | | **0** | Relies on reading | Helper text · tooltips · a warning in a modal that everyone dismisses | ## The rule that separates good UX poka-yoke from bad: undo beats confirm A confirmation dialog a user sees fifty times a day stops being a decision point. They develop click-through blindness and press "Confirm" with the same reflex they press "OK", which means the dialog protects nobody while adding friction to every legitimate action. It is the interface equivalent of a comment saying "be careful": present, visible, and inert. The preference order for destructive actions, strongest first: 1. **Make it reversible.** Soft-delete, trash with a retention period, version history. Now the mistake has no permanent consequence and needs no gate at all. This is the real answer and it is under-used because it is a backend change, not a UI change. 2. **Grace-period undo.** Perform it immediately, show "Deleted. Undo" for several seconds. No friction on the happy path, full recovery on the mistaken one. Its close cousin is delayed commit, hold the action for N seconds and drop it if undone, which is what Gmail's undo-send does, and that is the easier build when the operation cannot be reversed once performed. 3. **Require an action proportional to the consequence.** Typing the resource's name to confirm, GitHub's repository deletion, works because it cannot be done reflexively. Use it only for genuinely irreversible, high-blast-radius actions; used everywhere it becomes theater and people copy-paste through it. 4. **A confirmation dialog that states the specific consequence.** "Delete 3 projects and 1,204 files permanently?" is a real check. "Are you sure?" is not. It asks about resolve, not about facts, and the user's resolve is not the thing in question. A dialog that names the exact object and the exact count is doing fixed-value inspection. A dialog that says "This action cannot be undone" is doing nothing. ## Designing an interface: enumerate the mistakes first Same ritual as API design, different failure modes. Before laying out a screen, ask: 1. **What can the user enter that is wrong?** Can they even enter it? Free text where a constrained choice exists is a hazard: every free-text field is a place to be wrong. 2. **What is irreversible here?** Delete, send, publish, pay, cancel a subscription, rotate a key. Each needs a device from the list above, sized to its blast radius. 3. **What is adjacent to something dangerous?** "Save" beside "Delete" produces mis-clicks forever. Separate destructive actions spatially, style them differently, and never make them the default focus or the primary button. 4. **What does the user have to remember or carry between steps?** Anything they must hold in their head across a page transition will be dropped. 5. **What happens if they double-click, refresh mid-submit, or hit back?** Double submission is the UI's version of a non-idempotent retry, and it double-charges people. 6. **What is the state of this control when the data is missing, huge, or slow?** Empty, loading, error, and overflow states are where interfaces improvise. ## The devices **Constrain the input rather than validate it.** A picker instead of a text field, a stepper instead of a number input, a mask that only accepts a valid shape, `inputmode` and `type` so mobile keyboards offer the right keys, `max`/`min` that the control actually enforces. Every value the field cannot hold is a validation rule you never have to write and a user who never sees an error. **Disable the action until it can succeed**, but always show *why*. A greyed-out Submit with no explanation is its own dead end; pair it with the specific unmet requirement. Pick between the two shapes deliberately: native `disabled`, which takes the button out of the tab order, so the reason has to live in adjacent text a screen reader will reach anyway; or `aria-disabled` with the handler refusing the submit, which keeps the button focusable so the reason is announced on the control itself. **Validate at the right moment.** On blur for the field just left, never on every keystroke while someone is still typing, validating a half-typed email as invalid trains people to ignore your validation. Re-validate on submit, and put focus on the first offending field. **Preserve the user's work.** Losing entered data to a validation error, a session timeout, or a back button is one of the most common and most infuriating mistakes an interface permits. Draft autosave, restore-on-return, and never clear a form on a failed submit. **Make defaults safe rather than convenient.** The pre-selected option should be the one whose consequences are smallest if chosen inattentively, least-privilege, narrowest scope, private rather than public, opt-in rather than opt-out. Many users never change a default, so a default is a decision you are making for most of your users. **Prevent double submission structurally.** Disable the control on submit *and* carry an idempotency key on the request, because the button is not the only path, refresh, back, and a flaky network all retry. The UI device and the API device are the same hazard (M2 in the hazard catalog) seen from two sides. **Show scale before a bulk action.** "This will email 12,400 people" is fixed-value inspection and it stops the mistake that a confirmation dialog does not. ## Auditing an existing interface Read the actual component code, forms, buttons, modals, mutation handlers: not just screenshots. What to look for, in priority order: 1. **Every irreversible action.** Find the delete, send, publish, pay, and cancel handlers. For each: what device guards it, at what rung, and is the action recoverable at all? An irreversible action with only a generic confirm is the highest-value finding you will make. 2. **Every free-text input.** Could it be a constrained control instead? What happens with empty, whitespace-only, very long, pasted-with-formatting, or unicode input? 3. **Adjacency and defaults.** Is a destructive button next to a benign one, styled the same, or the default focus? Is any default the risky option? 4. **Submission paths.** Double-click, refresh mid-flight, back button, slow network. Is the mutation idempotent? 5. **Error handling.** When validation fails, is the user's input preserved, is focus moved to the problem, and does the message say how to fix it rather than what is wrong? 6. **Permissions.** Is a dangerous action merely hidden, or actually unavailable? Hiding a button is not a device: the endpoint is still there. Check that the server enforces it. Report using the same structure as `audit`: mistake, consequence, current rung, proposed device and rung. Propose before editing. ## Restraint Friction is a cost paid by every user on every legitimate use, and the mistake is made rarely. Confirmations on reversible actions, validation on optional fields, and are-you-sure dialogs on ordinary saves make an interface exhausting without preventing anything, and they train users to dismiss the dialogs that matter. Aim devices at what is irreversible and consequential; let everything else be fast, and make it undoable instead. The pattern reference at `../../references/ux-patterns.md` has the concrete forms of each device and the standard destructive-action patterns. The hazard catalog at `../../references/hazard-catalog.md` still applies to the code behind the screen: a mistake-proof form in front of a non-idempotent endpoint is only half a device.
Package details
Publisher declarations from the archived package. These are separate from our research and the live service's terms.
- Package license
- MIT
- Package author
- rainmanjam
- Keywords
- See publisher keywords
Declared capabilities
- Interactive
- Read
- Write
Package observed Oct 2, 2026.
Technical details
- First seen
- Sep 30, 2026 · 22:02 UTC
- Last seen
- Oct 3, 2026 · 00:00 UTC
- Collection status
- Collected
plugins_6a8ceaf162b88191851ca2442d67e12d
Download plugin data (JSON)