Poka Yoke
GitHubOfficialFirst committed by rainmanjamDistributed by GitHub
- Skill
- Guidelines
- Software development
- MIT
- Low risk
- Has scripts
AutoGineer summary
Written by AutoGineer from the skill’s own files, not by the author
Poka-yoke is a systematic approach to mistake-proofing code by replacing human vigilance with structural constraints. Rather than relying on documentation or warnings, this skill focuses on creating devices—such as type-safe APIs, exhaustive state machines, and boundary-level validation—that make invalid states unrepresentable. It provides a framework for auditing existing codebases to identify hazards, ranking them by their potential for silent failure, and implementing robust fixes that close entire classes of bugs rather than patching individual incidents.
What it does
- Provides a taxonomy of common coding hazards organized by contact, fixed-value, and motion-step lenses.
- Defines a four-rung hierarchy of error prevention, from control (impossible) to detection (after the fact).
- Offers a scanner to identify textually detectable hazards in codebases.
- Guides the transition from training-based fixes (comments/docs) to device-based fixes (types/constraints).
- Encourages pushing validation to the system boundary to catch errors at the source.
- Supplies language-specific patterns for Python, TypeScript, Rust, and Go.
When to use it
- When designing new interfaces, schemas, or state machines where reliability is critical.
- When auditing code for 'footguns' that are easy to misuse silently.
- When a bug has recurred and you need to close the entire class of error permanently.
- When working on high-stakes systems like payments, authentication, permissions, or data migrations.
- When you want to move from reactive debugging to proactive structural safety.
Advantages
- Reduces reliance on human memory and vigilance, which are prone to degradation.
- Creates self-documenting code where the design limits are enforced by the compiler or runtime.
- Provides a clear, objective framework for prioritizing technical debt and refactoring efforts.
- Prevents silent failures by ensuring errors announce themselves immediately.
- Improves code review quality by focusing on structural hazards rather than stylistic preferences.
Usage examples
- “Review this Python function for potential misuse: def transfer(from_id: str, to_id: str, amount: float): ...”
- The agent identifies the risk of swapping the two string IDs and suggests using NewType or keyword-only arguments to make the mistake impossible to compile.
- “How can I make my state machine for order processing more robust against invalid transitions?”
- The agent suggests using a discriminated union or exhaustive pattern matching to ensure every state transition is explicitly handled and illegal states cannot be constructed.
- “Audit this repository for silent failure hazards.”
- The agent uses the provided scanner to find unchecked external inputs or unbounded destructive operations and ranks them by blast radius.
- “We keep having issues with developers forgetting to set the region in our config. How do I fix this?”
- The agent recommends using a Pydantic-based settings model with no defaults, forcing the application to fail at startup if the configuration is incomplete.
Install
Download the skill as a ZIP:
https://autogineer.com/agent-skills/github/poka-yoke/downloadUnzip it into the cross-client skills folder of your project:
mkdir -p .agents/skills && unzip poka-yoke.zip -d .agents/skills
Documentation by rainmanjam
Poka-Yoke: Make the Mistake Unsayable
People will always make mistakes. That is not the problem worth solving. The problem is letting a mistake become a defect.
Shigeo Shingo, a Japanese industrial engineer, worked this out on a switch assembly line in 1961. Workers kept forgetting a spring. The fix was not a reminder: the job was split so the worker first laid both springs in a dish, then fitted them from the dish. A spring left over was the error announcing itself, before the unit could move on.
The dish is a device. "Please remember the spring" is not.
The line that does most of the work
A comment, a docstring, a wiki page, a review checklist, or a line in an instructions file saying "don't do X" is not a poka-yoke. It is training, and training degrades. A device does not. If your fix relies on someone remembering something, keep going.
This applies to your own instructions too. A rule written into a config file competes for attention with every other rule there and loses a little more as the file grows. A check that fails the build does not.
What this changes about the output
Given a design, models will readily list what to fix. They rarely state what the fix makes impossible, and that is the difference between advice you agree with and a constraint you can rely on. That habit is most of what this skill is for.
The other half is refusing to accept a non-device as a fix. "Add validation", "be careful with this function", "document the invariant" are all rung zero. Each has a real device behind it, and naming that device is the work.
Axis 1: what happens when the mistake occurs
Rank every finding on this ladder, and say which rung the current code sits on and which rung your fix reaches.
| Rung | Name | Meaning |
|---|---|---|
| 1 | Control | The wrong action cannot be performed. Type error, database constraint, missing permission. |
| 2 | Warning | It is possible, but announces itself as it happens. A linter, a runtime assertion, a confirmation you cannot skip. |
| 3 | Detection | It happens, and you find out afterwards. Tests, logging, monitoring, code review. |
| 0 | rung zero | Telling people to be careful. Docs, comments, "please remember to". |
Detection is not failure; sometimes it is all that is available. But a plan that stops at Detection should say so, rather than presenting it as prevention.
Axis 2: how the device notices
Shingo's three inspection lenses. They are a checklist for finding hazards, not decoration:
Contact — can the wrong thing physically fit? Two adjacent parameters of the same type can be swapped silently. A
stringthat should be one of four values. Money as a float.Fixed-value — is the set complete? A switch with no exhaustiveness check. A config where a missing key silently means "off". An enum handled in three of five places.
Motion-step — is the order right, and did every step happen? A two-phase write with no transaction. A retry with no idempotency key. A resource acquired on one path and released on another.
Inspect at the source
The cheapest place to catch a mistake is where it is made, not where it surfaces. A validation that runs three layers below the input has already let the bad value travel, and the stack trace will point at the wrong module. Push the check to the boundary the value crosses.
Designing something new
Mistake-proofing is cheapest before the code has callers. Once it has them, every device is a migration; before it has them, a device is free.
Work from the call site. A signature that reads fine in isolation often reads terribly where it is used:
# the mistake is expressible: nothing stops refunding an order that was never paid
def refund(order: dict) -> Refund:
return payments.refund(order["payment_id"])
# the mistake is no longer expressible
def refund(order: PaidOrder) -> Refund: ...The moves, roughly in order of how often they apply:
Make invalid states unrepresentable. A bag of optional fields where only certain combinations are legal becomes a discriminated union where the illegal ones cannot be constructed.
Parse, don't validate. Convert unstructured input into a type that carries proof at the boundary, once, rather than re-checking the same string in nine places.
Distinguish concepts that share a primitive. transfer(from: str, to: str) accepts its
arguments transposed. Distinct types for the two concepts, or keyword-only parameters, make the
transposition a compile error.
Encode the order. When calls must happen in sequence, let each step return the type the next step requires, so the wrong order does not typecheck.
Make the destructive path narrower than the safe one. A required, non-defaulting argument for the scope of a delete. A default that means "nothing" rather than "everything".
Close by naming what the design now makes impossible, and, just as importantly, what you deliberately left possible and why. A design whose limits are unstated will be trusted past them.
Auditing code that already exists
You are not looking for bugs. A bug is a mistake that already happened. You are looking for mistakes that are available: places where doing the wrong thing is easy, silent, and looks correct.
Run the bundled scanner first for the textually detectable shapes, then read for the ones no scanner can see:
python3 scripts/detect_hazards.py --paths . # whole tree
python3 scripts/detect_hazards.py --staged # pre-commit
python3 scripts/detect_hazards.py --diff --json # CI, exits non-zero on findings
python3 scripts/detect_hazards.py --severity highNo dependencies, so it runs in CI and in a pre-commit hook without an install step. It reports what it scanned: a scan of zero files exits non-zero rather than reporting a clean bill of health, because an all-clear you got by typo is worse than no check.
Rank findings by blast radius times ease of the mistake. An unchecked value reaching a write, a delete, a payment or an auth decision outranks one that can only produce a clean crash. For each finding, state: where it is, what the mistake is, what the consequence is, what device exists today, what device would close it, and which rung that reaches.
references/hazard-catalog.md is the taxonomy of shapes with their IDs and devices.
Language-specific patterns are in references/lang-python.md, references/lang-typescript.md
and references/lang-rust-go.md.
After an incident
Separate three things that get conflated, because the fix belongs to the third:
Defect — what the user experienced.
Mistake — the specific wrong action someone took.
Hazard — the property of the system that made that mistake available.
Fixing the mistake fixes one case. Fixing the hazard fixes the class. Then sweep: the same shape almost certainly exists elsewhere, and finding the second and third instance is the difference between a patch and a lesson.
Attribute cause to the system rather than to a person. Not primarily for kindness: "they made a mistake" is a complete-sounding explanation that predicts nothing and prevents nothing, and it ends the investigation early.
What good output looks like
Anchored to lines.
orders.py:142, apply_discountis reviewable; "the discount logic" is not.Ranked, with the ranking visible, so a reader who stops halfway has still covered the ones that matter.
A named device per finding, not "add validation".
The rung stated, before and after.
The limits stated. What the fix does not cover is the part readers most need and most often do not get.
Sized honestly. Three findings that matter beat eleven padded to a round number.
What to avoid
Accepting rung zero as a fix. If the proposal is a comment, a doc, or a convention, the work is not finished.
Devices nobody can bypass being confused with devices nobody does bypass. A pre-commit hook
is skippable with --no-verify; it needs CI behind it to be a real gate. Say which one you are
proposing.
Over-fitting to one incident. Machinery that prevents one specific failure must itself be understood and maintained. Ask whether the shape is common enough to justify it.
Treating monitoring as prevention. Detection lowers the cost of a failure; it does not lower the likelihood. Both are worth having, and conflating them means the likelihood never gets addressed.
Evidence, and its limits
This method was benchmarked at 591 blind-graded runs across six model families, scored against assertions written before the runs by a grader that never saw which configuration produced a response. The behaviour it most reliably changes is stating what a design forecloses: 45% of responses did that unprompted, 80% with the method applied, across 132 graded verdicts.
That average conceals where the effect lives. Asked squarely to design an interface, models already do it 77% of the time; the skills add eleven points. The large gains are in tasks where nobody asked for a design review — writing an endpoint goes 14% to 79%, shipping an agent feature 33% to 83%, building a form 29% to 64%.
Stated honestly, because the limits matter: every run was the first turn of a fresh session, so this measures the ceiling rather than what survives a long working session. The comparison was against no methodology at all, not against a different one, so it does not establish that this method is what produced the gain. And the method costs something measurable: responses became somewhat worse at spotting the specific defect already on the page while becoming better at changing the shape that allowed it. If you want the bug in front of you found, use a reviewer. If you want that class of bug to stop being expressible, use this.
Raw runs, the harness and the assertion checklists are at https://github.com/rainmanjam/poka-yoke.
Files
- ATTRIBUTION.mdAdded by AutoGineer812 B
- LICENSELicence file added by AutoGineer1.0 KiB
- SKILL.md10.9 KiB
- references/hazard-catalog.md20.3 KiB
- references/lang-python.md6.7 KiB
- references/lang-rust-go.md7.6 KiB
- references/lang-typescript.md5.9 KiB
- scripts/detect_hazards.pyScript26.2 KiB
references/lang-rust-go.md
Rust and Go Devices
Two languages at opposite ends of the expressiveness spectrum. Rust can encode almost any invariant in types; Go deliberately cannot, so its devices lean on convention plus tooling. Know which one you're in before proposing a device.
Rust
Rust's type system reaches Control for more hazard classes than any other mainstream language.
The affine type system in particular is the only mainstream answer to use-after-move, and it
turns use-after-close into a compile error rather than a convention, where Python has context
managers, TypeScript has scope-bound callbacks, and Go has defer, Rust has the compiler.
Contact, newtypes and smart constructors
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct UserId(Uuid);
#[derive(Debug, Clone)]
pub struct Email(String);
impl Email {
// The only way to build one. Private field means no bypass, even in-crate
// if you put it behind a module boundary.
pub fn parse(s: &str) -> Result<Self, InvalidEmail> {
if !s.contains('@') { return Err(InvalidEmail); }
Ok(Email(s.to_owned()))
}
pub fn as_str(&self) -> &str { &self.0 }
}A private field plus a fallible constructor means possessing an Email is proof of
validation. This is the strongest form of parse-don't-validate available anywhere.
Contact, enums make illegal states unrepresentable
// Each variant carries exactly the data that variant has. There is no
// "succeeded with an error", because it cannot be written.
pub enum JobState {
Queued { enqueued_at: DateTime<Utc> },
Running { started_at: DateTime<Utc>, worker: WorkerId },
Succeeded { output: Output },
Failed { error: JobError, retries: u32 },
}match without a catch-all is exhaustive by default, adding a variant breaks the build
everywhere it must. Avoid _ => {} arms in domain logic for exactly this reason: the wildcard
is what turns a compile error into a silent fallthrough two releases later.
Motion-step, typestate
Ownership makes typestate genuinely practical, since each transition consumes the old state:
pub struct Draft;
pub struct Validated;
pub struct Order<S> { items: Vec<Item>, _state: PhantomData<S> }
impl Order<Draft> {
pub fn validate(self) -> Result<Order<Validated>, ValidationError> { /* … */ }
}
impl Order<Validated> {
// submit() does not exist on Order<Draft>. Not "returns an error", does not exist.
pub fn submit(self) -> Result<OrderId, SubmitError> { /* … */ }
}The consumed self means the draft is gone after validation, so a stale unvalidated copy
cannot be submitted later.
Fixed-value, make errors impossible to ignore
#[must_use] on Result is built in; add it to your own types where dropping the value is a
bug. Then set the lints:
[workspace.lints.clippy]
unwrap_used = "deny"
expect_used = "warn" # allow in tests and startup with a reason
panic = "deny"
indexing_slicing = "deny" # forces .get() and a real branch
float_cmp = "deny"
arithmetic_side_effects = "warn" # forces checked_/saturating_ where overflow matters
todo = "deny"
dbg_macro = "deny"unwrap_used = "deny" is the highest-value line in that block: it converts every "this can't
fail" assumption into an explicit decision at review time.
Rust limits
unsafeandunwrapare the escape hatches. Deny both by lint and require a// SAFETY:comment for eachunsafeblock.Compile-time only. Deserialized input still needs
serdewithdeny_unknown_fields.Panics bypass the type system. A device that panics is Warning, not Control.
Typestate has real ergonomic cost. Reserve it for genuinely dangerous sequences, payments, resource lifecycles, protocol state: not for every builder.
Go
Go rejects most compile-time expressiveness by design. Its devices are therefore fewer, and tooling plus data-layer constraints carry more of the load. Say so plainly when you propose a device, Control is often not reachable here, and pretending otherwise is worse than acknowledging the rung.
Contact, defined types
type UserID string
type OrderID string
func Transfer(from, to UserID) error { ... }
// Transfer(orderID, userID), compile error, because these are defined types, not aliases.Use type X string (a defined type), never type X = string (an alias, which gives you
nothing). This is the one genuine Control-rung contact device Go offers, and it is
underused.
Contact, functional options instead of boolean flags
type Option func(*Config)
func WithTimeout(d time.Duration) Option { return func(c *Config) { c.Timeout = d } }
func WithRetries(n int) Option { return func(c *Config) { c.Retries = n } }
func New(addr string, opts ...Option) (*Client, error) { ... }Every option is named at the call site, time.Duration carries its unit in the type, and
adding an option later doesn't break callers. This replaces both the boolean-flag hazard and
the implicit-units hazard.
Motion-step, constructors and defer
func NewClient(addr string) (*Client, error) {
// Fully ready on return. No Connect() to forget.
}
conn, err := pool.Acquire(ctx)
if err != nil { return err }
defer conn.Release() // on the line after acquisition, alwaysPut defer immediately after the acquisition, before any other statement. Any code between
the two is a leak on the error path.
Fixed-value, exhaustiveness
Go has no exhaustive switch. Use a linter:
# .golangci.yml
version: "2"
linters:
enable:
- errcheck # unchecked errors: the single most valuable Go linter
- exhaustive # non-exhaustive switch over typed constants
- bodyclose # unclosed HTTP response bodies
- rowserrcheck # unchecked sql.Rows.Err
- sqlclosecheck
- contextcheck # context not propagated
- nilerr # returning nil after a non-nil error
- noctx # HTTP requests without a context
- gosec
settings:
exhaustive:
default-signifies-exhaustive: falseThat is the v2 schema. golangci-lint v2 refuses to run against a v1 file rather than ignoring
the parts it no longer understands, so run golangci-lint migrate over an existing config
before upgrading.
errcheck is non-negotiable, Go's error convention is entirely opt-in without it, and
_ = doSomething() is how data loss enters a Go codebase.
Go-specific traps
Nil maps accept reads but panic on write. Construct with
makein the constructor.Loop variable capture in goroutines, fixed in Go 1.22+, still present in older codebases and vendored code.
time.Durationvs bare int: always take aDuration; never anintseconds.Zero values are valid, so a struct with a missing field looks initialized. Use a constructor that returns
(T, error)and unexported fields to force it.Slices share backing arrays,
appendto a sub-slice can mutate the original. Use three-index slicings[a:b:b]when handing a slice out.context.Contextdropped across a call boundary silently disables cancellation and timeouts.contextcheckcatches it.
Go limits
Go cannot express: exhaustive matching, non-nullable references, immutability, typestate, or generic constraints rich enough for units. Its Control-rung devices are essentially defined types, unexported fields with constructors, and the database schema.
The practical consequence: in Go, push more invariants into the database and into required
CI checks than you would in Rust or TypeScript. NOT NULL, CHECK, and unique constraints
are doing work the language declines to do, and golangci-lint as a required check is what
makes the rest hold.
Requirements and compatibility
Written for: GitHub Copilot
Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta
“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.
Needs: Needs Python
The author says: Cross-platform. The bundled scanner needs Python 3.9+ and no third-party packages. Everything else is language-agnostic guidance; worked examples are TypeScript, Python, Go, Rust and SQL.
Security
What our scanner noticed· Low risk
- Scripts that your agent may run
This skill includes scripts that your agent may run. Read them before you use it.
A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.
Attribution and licence
- First committed by
rainmanjam
- Publisher
- GitHub (Official)
- Distributed by
- GitHub
- Source
- github/awesome-copilot · skills/poka-yoke · commit 797337f
- Licence
- MIT License (redistribution allowed)
- LICENSE (added by AutoGineer from LICENSE)
- ATTRIBUTION.md (written by AutoGineer)
- Synced
- Contributors
- rainmanjam
AutoGineer is not affiliated with or endorsed by rainmanjam or GitHub.
Download ZIP
The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download poka-yoke.zip