Minimal Repro Cases skill for Codex

Shrink any bug to one file a maintainer will run. Codex fixes an exact failure signature before deleting anything, records versions, checks whether the latest release already fixed it, then removes services, framework and data layer by layer. Large inputs are shrunk with a bundled dependency-free ddmin script or cvise and shrinkray, and the result is rebuilt in a clean uv, npm or Docker environment and written up as an upstream-ready issue.

  • Engineering Workflow
  • $minimal-repro-cases
  • 201 lines
  • 3 files
  • ~2,050 tokens
  • MIT
  • Updated

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- minimal-repro-cases

Installs to ~/.agents/skills/minimal-repro-cases/ for every project on this machine.

104-line POSIX script. No sudo, no telemetry. Read install.sh

Use it in Codex

Start a prompt with $minimal-repro-cases, or pick it from /skills. You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.

What Codex reads before loading it

Shrinks a bug to the smallest self-contained reproduction - one file or one command, pinned versions, expected versus actual output - by fixing an exact failure oracle, stripping services and dependencies outside-in, and delta-debugging inputs with a bundled ddmin script, C-Reduce, cvise or shrinkray. Produces a ready-to-file upstream issue. Use when filing a bug against a library or framework, when a failure only happens inside a large app, when a maintainer asks for a minimal reproduction, or when a crashing input file is too big to reason about.

Use it when

  • You are about to file a bug against a library, framework or tool
  • A maintainer or teammate asked for a minimal reproduction
  • A failure needs a huge input file or the whole app to show up
  • You need to prove whether a bug is yours or upstream

Use something else when

How to check the result

Run the repro in a fresh directory or container and confirm the original error signature appears, then confirm that deleting any remaining line makes it pass.

The mistake it prevents

Shrinking with a loose oracle such as 'exits non-zero', which lets the reducer wander into a different, smaller bug than the one you started with.

~/.agents/skills/minimal-repro-cases/SKILL.md

--- name: minimal-repro-cases description: Shrinks a bug to the smallest self-contained reproduction - one file or one command, pinned versions, expected versus actual output - by fixing an… license: MIT ---

A repro is done when a stranger can run one command on a clean machine, see the same failure signature, and nothing left in it can be removed without the failure going away. It names exact versions, states expected versus actual, and contains no private code, credentials or customer data.

Workflow

- [ ] 1. Capture the failure and define the oracle (exact signature)
- [ ] 2. Record the environment and pin versions
- [ ] 3. Check it still fails on the latest release / main
- [ ] 4. Remove the app around it, outside-in
- [ ] 5. Shrink the input (ddmin, cvise, shrinkray)
- [ ] 6. Shrink the code by delete-and-retest
- [ ] 7. Rebuild from scratch in a clean environment and rerun
- [ ] 8. Write the report; get approval before posting upstream

1. Define the oracle first

Shrinking without a precise oracle drifts into a different bug. The oracle is a command that exits 0 when the bug is present (“still interesting”) and non-zero otherwise. Match the most specific signal you have:

Failure kindOracle
ExceptionException type plus a stable fragment of the message, not the line number
Wrong outputCompare against the expected value; interesting when they differ in the known way
Compiler or tool crashExit code plus the internal error text (internal compiler error, panicked at)
Hangtimeout 10 <cmd>; interesting when the exit code is 124
FlakyLoop N runs; interesting when the failure count is above zero. Measure the base rate first
#!/usr/bin/env bash
# interesting.sh: exit 0 only while the original bug reproduces
out=$(python3 repro.py 2>&1)
grep -q "KeyError: 'tenant_id'" <<<"$out"

2. Record the environment

Capture versions before changing anything; repros fail to transfer mostly because of versions nobody wrote down.

python3 -VV && python3 -m pip freeze | grep -iE "^(sqlalchemy|psycopg)"
node -v && npm ls <package>          # or: npx envinfo --system --binaries --npmPackages <pkg>
go version && go env GOOS GOARCH
rustc -vV && cargo tree -i <crate>
uname -a                              # OS and architecture

Note anything environmental the failure might depend on: locale, timezone, CPU arch, filesystem case sensitivity, container vs host.

3. Test the latest version

Before investing in shrinking, try the newest release (and main if cheap). If it is already fixed, the deliverable is “upgrade to X.Y.Z” plus the changelog entry, not a repro. If a version range is known good and bad, hand the range to $git-bisect on the upstream repo. Installing new versions needs network; in a Codex sandbox without it, ask for approval or report which versions you could test.

Search upstream issues with the exact error text before writing a new one:

gh search issues --repo owner/project "KeyError tenant_id" --limit 20   # open and closed

4. Strip the app, outside-in

Remove whole layers first; they are the biggest wins. After each removal, rerun the oracle. If the failure disappears, the removed thing is involved: put it back and remove something else.

  1. Network services: replace the HTTP API, queue or cache with a hardcoded value or an in-memory fake. Databases: try SQLite or a single docker run postgres:17 with one table, created in the repro.
  2. Framework: call the library function directly instead of through the web request, CLI or job runner.
  3. Your own code: inline the few lines that matter; delete the rest.
  4. Configuration: start from defaults and add back only the options needed.
  5. Data: replace real records with two or three synthetic ones that keep the shape that triggers the bug (the null field, the 256-byte string, the emoji).

5. Shrink inputs mechanically

When the trigger is a large input file (query, source file, JSON document, CSV), let a tool do the deleting. The bundled script runs ddmin over lines with no dependencies:

python3 scripts/ddmin_lines.py big_input.sql \
  --cmd "python3 run_query.py {file}" --expect-output "IndexError" --timeout 20
# -> lines: 3120 -> 7  runs: 412  written: big_input.min.sql

{file} is a temporary copy with the original basename. Use --expect-code N for an exact exit code; with neither flag, any non-zero exit counts. The script refuses to run if the unmodified input does not reproduce, which catches a broken oracle early.

InputTool
Any line-oriented textscripts/ddmin_lines.py
C, C++ and other source codecvise ./interesting.sh file.c (or creduce); both expect the test script to exit 0 while interesting and run it in a temp copy
Arbitrary text or binary, structure-awareshrinkray ./interesting.sh file (pip package)
Generated test input from property testsLet Hypothesis, fast-check or proptest shrink it; copy the shrunk example
JSONDelete keys and array items by hand, or flatten to lines and use ddmin

Then hand-polish: rename identifiers to neutral names, replace magic values with obvious ones, but rerun the oracle after each edit.

6. Shrink the code

Repeat until no single deletion keeps the failure: delete a statement, import, option or argument, rerun, keep the deletion if still interesting. Also try replacing values with simpler ones ([], 0, "", None). Stop when every remaining line is load-bearing; say so in the report.

7. Rebuild clean

A repro that only works in your checkout is not a repro. Recreate it from nothing:

# /// script
# requires-python = ">=3.12"
# dependencies = ["sqlalchemy==2.0.36"]
# ///
# Run with: uv run repro.py
from sqlalchemy import create_engine, text
engine = create_engine("sqlite://")
with engine.connect() as c:
    print(c.execute(text("select :x"), {"x": 1}).scalar())
EcosystemClean template
PythonOne file with PEP 723 inline metadata, run with uv run repro.py
Nodepackage.json with exact versions plus repro.mjs; npm ci && node repro.mjs
Browser or frameworkA minimal starter (Vite, framework CLI) with one changed file, pushed to a public repo or StackBlitz
Gogo mod init repro with one main.go or _test.go, versions in go.mod
Rustcargo new repro, exact versions in Cargo.toml
System-dependentA Dockerfile with a pinned base image digest or tag

Run it in a fresh directory (or docker run --rm) and confirm the oracle passes there.

Gotchas

  • Oracle drift: ddmin happily finds a smaller input that fails with a different error. Match the specific message, and compare the final stack trace to the original.
  • Removing the trigger: timing and concurrency bugs vanish when the surrounding load is removed. Keep a loop or a deliberate sleep and say the repro is probabilistic, with the observed failure rate.
  • Test-order bugs: a test that fails only in the full suite is pollution. Narrow the polluter by bisecting the preceding tests (detect-test-pollution for pytest), then repro with exactly two tests.
  • Private data: real customer rows, internal hostnames, tokens and proprietary code must never reach a public issue. Replace with synthetic values and check the final files.
  • Caches: stale __pycache__, node_modules/.cache, build dirs and Docker layers can keep a fixed bug alive or hide a real one. Clean them before the final run.
  • Debug builds and release builds can differ; state which one reproduces.

Report template

Posting an issue or comment upstream is outward-facing: show the draft and get a clear yes before running gh issue create.

### Summary
`Session.merge()` raises KeyError when the primary key column is named `tenant_id`.

### Reproduction
```bash
uv run repro.py
```
<contents of repro.py, under ~40 lines>

### Expected
Prints `1`.

### Actual
```
KeyError: 'tenant_id'
  File ".../orm/session.py", line 3871, in merge
```

### Environment
- package 2.0.36 (also fails on 2.1.0b2 and main @ 4c1d2e9); 2.0.30 works
- Python 3.12.7, macOS 15.1 arm64 and Debian 12 x86_64

### Notes
- Every line in repro.py is required; removing any one makes it pass.
- Fails 100% of runs.

What the zip contains

Codex reads bundled files only when a step points to them. Every one is shown here, so you can review it before installing.

agents/openai.yamlgenerated from this page

Gives the Codex app a display name, a one-line description and a starter prompt for this skill.

interface:
  display_name: "Minimal Repro Cases"
  short_description: "Shrink any bug to one file a maintainer will run"
  default_prompt: "$minimal-repro-cases This crash only happens in our monolith. Get me a minimal repro I can file against the library"
scripts/ddmin_lines.py115 lines, 4.5 KB
#!/usr/bin/env python3
"""Shrink a text file line by line while a command keeps reproducing a failure.

Implements Zeller's ddmin over lines. A candidate is "interesting" when the
command's exit code and/or output still match the original failure. The input
file is never modified; the result is written to --out.

Usage:
  python3 ddmin_lines.py input.py --cmd "python3 {file}" --expect-output "KeyError"
  python3 ddmin_lines.py query.sql --cmd "psql -f {file} db" --expect-code 3

{file} is replaced by the path of a candidate copy that keeps the original
basename (in a temporary directory), so extension-sensitive tools still work.
Standard library only; Python 3.10+.
"""
from __future__ import annotations

import argparse
import shlex
import subprocess
import sys
import tempfile
from pathlib import Path


def make_test(cmd: str, workdir: Path, name: str, expect_code: int | None,
              expect_output: str | None, timeout: float, cwd: str | None):
    calls = {"n": 0}
    cache: dict[tuple[str, ...], bool] = {}
    target = workdir / name

    def interesting(lines: list[str]) -> bool:
        key = tuple(lines)
        if key in cache:
            return cache[key]
        calls["n"] += 1
        target.write_text("".join(lines))
        argv = shlex.split(cmd.replace("{file}", shlex.quote(str(target))))
        try:
            proc = subprocess.run(argv, capture_output=True, text=True,
                                  timeout=timeout, cwd=cwd)
        except subprocess.TimeoutExpired:
            cache[key] = False  # hangs are a different bug; do not drift into them
            return False
        ok = True
        if expect_code is not None:
            ok = ok and proc.returncode == expect_code
        else:
            ok = ok and proc.returncode != 0
        if expect_output is not None:
            ok = ok and expect_output in (proc.stdout + proc.stderr)
        cache[key] = ok
        return ok

    return interesting, calls


def ddmin(lines: list[str], test) -> list[str]:
    n = 2
    while len(lines) >= 2:
        size = len(lines) // n
        chunks = [lines[i:i + size] for i in range(0, len(lines), size)] if size else [[l] for l in lines]
        reduced = False
        for i, chunk in enumerate(chunks):
            if test(chunk):  # a single chunk reproduces on its own
                lines, n, reduced = chunk, 2, True
                break
        if not reduced:
            for i in range(len(chunks)):
                complement = [l for j, c in enumerate(chunks) if j != i for l in c]
                if test(complement):  # removing chunk i keeps the failure
                    lines, n, reduced = complement, max(n - 1, 2), True
                    break
        if not reduced:
            if n >= len(lines):
                break
            n = min(len(lines), n * 2)
    return lines


def main() -> int:
    ap = argparse.ArgumentParser(description=__doc__.splitlines()[0])
    ap.add_argument("input", type=Path)
    ap.add_argument("--cmd", required=True, help='command with {file}, e.g. "python3 {file}"')
    ap.add_argument("--expect-code", type=int, default=None,
                    help="exact exit code that means 'still fails' (default: any non-zero)")
    ap.add_argument("--expect-output", default=None,
                    help="substring that must appear in stdout+stderr")
    ap.add_argument("--timeout", type=float, default=30.0, help="seconds per run")
    ap.add_argument("--cwd", default=None, help="directory to run the command in")
    ap.add_argument("--out", type=Path, default=None, help="output path (default: <stem>.min<suffix>)")
    args = ap.parse_args()

    if "{file}" not in args.cmd:
        print("error: --cmd must contain {file}", file=sys.stderr)
        return 2
    original = args.input.read_text().splitlines(keepends=True)
    out = args.out or args.input.with_name(f"{args.input.stem}.min{args.input.suffix}")

    with tempfile.TemporaryDirectory() as tmp:
        test, calls = make_test(args.cmd, Path(tmp), args.input.name, args.expect_code,
                                args.expect_output, args.timeout, args.cwd)
        if not test(original):
            print("error: the unmodified input does not reproduce the failure with this "
                  "--cmd/--expect-* combination; fix the oracle first", file=sys.stderr)
            return 1
        result = ddmin(original, test)

    out.write_text("".join(result))
    print(f"lines: {len(original)} -> {len(result)}  runs: {calls['n']}  written: {out}")
    return 0


if __name__ == "__main__":
    sys.exit(main())

About this skill

Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.

esc
Just launched

Codex Skills Pro is live

79 premium skills for Codex, each with scripts, references and an evidence report that proves the result.

Bought one by one$882.21
All 79 skills$49
94% off

That's $0.62 a skill, paid once.

Get all 79 for $49Or pick single skills from $4.99 →

Secure checkout by Creem