A disciplined workflow for planning, executing, and verifying non-trivial code changes safely. Use this skill whenever the user asks for a code change that is hard, risky, multi-file, or ambiguous — including bug fixes, refactors, feature additions, dependency upgrades, migrations, performance work, or "fix this failing test" requests. Also use it when entering an unfamiliar codebase, when the blast radius of a change is unclear, or when the user asks you to "be careful", "don't break anything", or to explain what you changed and why. Do NOT skip this skill just because the change looks small — small diffs in unfamiliar code are where regressions hide.
A skill that turns "make this change" into a sequence of observable, verifiable
steps: inspect → plan → change minimally → verify → report with evidence.
Core principle: never claim something works that you have not observed
working. Every claim in the final report must be backed by a command you ran,
output you read, or a file you inspected. Reasoning is expressed as visible
artifacts (notes, plans, command output), not as private deliberation.
When to use
Use this workflow when ANY of the following are true:
The task touches more than one file, or you don't yet know how many files it touches.
You are new to this codebase or this area of it.
The change affects public APIs, data models, persistence, auth, money, concurrency, or anything user-facing.
The task description is ambiguous, contradictory, or missing acceptance criteria.
Tests exist and could break, or tests don't exist and should.
The user asked for a "quick fix" in code you haven't read yet. (Especially then.)
You may abbreviate the workflow (not skip verification) only when the change is
a single-file, single-purpose edit in code you have already read this session,
with a fast test or run command available.
Step-by-step workflow
Phase 0 — Restate the task
Before touching anything, write down (in your working notes or response):
Goal: one sentence describing the desired end state.
Acceptance criteria: how you will know it's done (specific behaviors, passing tests, output).
Explicit constraints: anything the user said not to do (no new deps, keep API stable, etc.).
Open questions: anything ambiguous. Decide per the "missing information" rules below whether to ask or proceed with a stated assumption.
If you cannot state acceptance criteria, you are not ready to edit code.
Phase 1 — Inspect before editing
Never edit a file you haven't read. Never assume structure you haven't verified.
Run a reconnaissance pass and record what you find:
# Layout and entry points
ls / tree (2 levels), README, docs/
# Project type, dependencies, versions
package.json / pyproject.toml / go.mod / Cargo.toml / pom.xml ...
lockfiles (to see what's actually pinned)
# How the project is built, run, and tested
Makefile, scripts section, CI config (.github/workflows, .gitlab-ci.yml)
# Conventions and constraints
linter/formatter configs, tsconfig/mypy strictness, CONTRIBUTING.md, .editorconfig
# State of the tree
git status, git log --oneline -10 (am I on a dirty tree? recent related changes?)
Then narrow to the task area:
Locate the code paths involved (grep for the function/route/error message).
Read the relevant files fully, not just the matching lines.
Find the callers and callees of anything you plan to change (grep -rn, IDE-equivalent search). List them.
Find existing tests for this area and run them before changing anything, so you know the baseline. A test that was already failing is not your regression — but you must know that before you edit.
Record a short written map: "The request flows A → B → C; the bug is likely in B because ...; B is called from X and Y."
Phase 2 — Decompose into safe steps
Break the task into steps where each step:
Has a single purpose (one behavior change, one refactor, one dependency bump — never mixed).
Leaves the project in a working or at least buildable state.
Is independently verifiable (a command that proves it worked).
Is reversible (small enough to revert without archaeology).
Order steps risk-first when possible: do the investigation and scaffolding (add a failing test that reproduces the bug) before the fix, and do behavior-preserving refactors in separate steps from behavior changes.
Write the plan down as a numbered list with a verification command per step. Example:
1. Add failing test reproducing #142 (verify: pytest tests/test_auth.py -k expired — fails)
2. Fix token expiry comparison in auth/session.py (verify: same test passes)
3. Run full auth test suite (verify: pytest tests/test_auth.py — all pass)
4. Run linter + typecheck (verify: ruff check, mypy — clean)
Phase 3 — Detect edge cases and hidden risks
Before implementing, explicitly check the task against this list and write down which apply:
For each applicable risk, either handle it in the plan, cover it with a test, or explicitly note it as out of scope in the report.
Phase 4 — Implement with safe editing rules
(See "Safe code editing rules" below.) Work step by step through the plan, running the step's verification command before moving on. If a step's verification fails, stop and fix or replan — do not stack changes on a broken state.
Phase 5 — Verify the whole
Run the full verification checklist (below). Verification of individual steps is not sufficient; interactions between steps are where regressions live.
Phase 6 — Report with evidence
Produce a final report in the reporting format (below). Every claim of success must cite the command and its observed result.
Safe code editing rules
Read before write. Read the entire function/class you're editing and its immediate neighbors. Never edit from a grep snippet alone.
Minimal diff. Change only what the task requires. No drive-by reformatting, renaming, or "improvements" outside scope — they pollute review and hide the real change. If you notice unrelated problems, record them in the report instead of fixing them silently.
Match existing conventions. Follow the project's style, naming, error-handling patterns, and test structure, even where you'd personally choose differently.
Preserve public contracts. Don't change function signatures, API shapes, config keys, serialized formats, or exported names unless the task explicitly requires it. If you must, list every caller you updated and how you found them.
No silent behavior changes. If a fix necessarily changes observable behavior beyond the bug (error messages, ordering, defaults), call it out explicitly.
No new dependencies without justification. Prefer the standard library and existing project deps. If a new dependency is genuinely needed, state why, check its license and maintenance status, and pin the version.
Never weaken safety to make things pass. Do not delete or skip failing tests, loosen assertions, broaden except/catch blocks, disable lint rules, or add # type: ignore to silence errors — unless the test/rule itself is demonstrably wrong, in which case say so in the report with reasoning.
No destructive operations without an explicit go-ahead. Do not delete files/branches, run migrations against shared databases, force-push, rewrite history, or touch production systems or credentials on your own initiative.
Keep the tree recoverable. Work on a clean tree where possible; commit or checkpoint at safe boundaries so any step can be reverted alone.
Handle generated and vendored code correctly. Never hand-edit generated files (protobufs, lockfiles, build output); change the source and regenerate.
Secrets stay out. Never hardcode credentials, tokens, or user data in code, tests, fixtures, or logs.
When a "simple fix" grows, stop. If the diff is spreading beyond the plan, return to Phase 2 and replan rather than improvising.
Deciding what to do next when information is missing
Classify the gap, then act:
Gap type
Action
Answer exists in the repo (behavior, conventions, structure)
Investigate. Read code, run it, write a scratch test. Don't ask the user things the codebase can answer.
Answer exists in docs of a dependency/tool
Look it up (docs, changelog, source in the lockfile's pinned version).
Product/intent decision (which behavior is desired), destructive or irreversible action, scope expansion, security tradeoff
Ask the user. Present the options you see and your recommendation.
Low-stakes ambiguity where any reasonable reading is fine
Proceed with a stated assumption. Write the assumption in your plan and repeat it prominently in the final report so it can be corrected cheaply.
Stop and report exactly what you tried, what failed, and what you need — do not fabricate a workaround that pretends the blocker is gone.
Rule of thumb: investigate facts, ask about intent, state assumptions you make, and never let an unstated assumption reach the final report.
Verification checklist
Do not claim completion until every applicable item is checked, each backed by an actually executed command and observed output:
Baseline recorded: pre-change test/build status was captured, so new failures are distinguishable from pre-existing ones.
The change is exercised: at least one test or manual run demonstrably executes the new/changed code path (not just "tests pass" — tests that never touch your code prove nothing).
New/updated tests fail without the fix (for bug fixes: you saw the test fail on the old code, then pass on the new).
Relevant test suite passes: run the project's own test command (from CI config or scripts), not a guessed one.
Build/compile/typecheck passes with the project's real configuration.
Linter/formatter passes, using the project's config.
Edge cases from Phase 3 are covered by tests or explicitly noted as out of scope.
Diff reviewed: read the full git diff yourself; confirm no unintended files, debug prints, commented-out code, TODO placeholders, or secrets.
Callers audited: every caller of changed interfaces was found and updated or confirmed unaffected.
No verification theater: no skipped/disabled tests, loosened assertions, or suppressed warnings introduced to make checks green.
Pre-existing failures listed: anything that was already broken before your change is documented as such, with evidence of the baseline run.
If an item cannot be verified in this environment (e.g., no test runner available), the report must say so explicitly rather than implying it passed.
When the project has no test suite
Many real projects — scripts, document pipelines, agent systems, internal tools, prototypes — have no pytest/jest suite, no typechecker, and no linter config. The core rule does not relax: never claim something works that you have not observed working. Only the instruments change. Substitute these, cheapest first, and write the chosen command into the plan exactly as you would a test:
Load check — proves the code still parses and imports, in about a second:
python3 -c "import pkg.module" · node --check file.js · bash -n script.sh · tsc --noEmit · jq empty config.json
Smoke run — start the thing and touch it once: run the CLI with --help, curl the health endpoint and read the status code, open the page. Record the actual output line, not "it ran".
Targeted end-to-end script — for UI/browser/API work, a short script that drives the real thing (Playwright, curl, a scratch client) and prints explicit pass/fail per assertion. Keep the script — it is the regression test the project didn't have, and the next change gets it for free.
Before/after evidence on the same command — screenshot, JSON output, or diff of identical invocations run before and after the change. This is how you get a baseline when there are no tests to run.
Verify at the destination — if the change ships (deploy, sync, publish), check there too: local success is not deployment success.
Verify the delivery step itself — after git push/deploy, confirm the remote state actually moved (git status -sb showing no unpushed commits, a fetched health endpoint, the file visible at the destination). Reporting "pushed" because the command exited 0 without checking the resulting state is the same failure class as claiming a test passed without running it.
Two things that look like verification but are not: reading the diff and finding it correct (that is review, not execution), and the tool reporting success (an editor writing a file proves the write, not the behavior).
If none of these are possible, say so plainly in the report — "no way to execute this here; unverified" — and never let an unverified claim sit in a Summary line.
Reporting format
Final reports use this structure:
## Summary
One paragraph: what was asked, what was done, current status
(✅ complete / ⚠️ complete with caveats / ❌ blocked).
## Changes
- `path/to/file.py` — what changed and why (one line per file)
## Evidence
For each verification claim: the command run and the observed result.
Quote the decisive lines of output, not walls of logs.
## Assumptions & decisions
Assumptions made where information was missing; alternatives considered
and why the chosen approach won.
## Risks & out of scope
Known limitations, edge cases deliberately not handled, pre-existing
issues noticed but not fixed, and anything the reviewer should look at
extra carefully.
## Suggested follow-ups (optional)
Improvements observed but intentionally not made.
Rules for reports:
Every "passes" / "works" / "fixed" claim links to an Evidence entry.
Distinguish clearly between verified ("ran X, saw Y") and expected ("should also work for Z, not tested here").
Report failures and partial completion honestly and prominently — a precise "blocked" report is a success; a vague "done" that isn't is a failure.
Examples of good final reports
Example 1 — Bug fix, fully verified
## Summary
Fixed #142: sessions with tokens expiring exactly at the request timestamp were
accepted instead of rejected. Root cause was an inclusive comparison in
`Session.is_valid`. Status: ✅ complete.
## Changes
- `auth/session.py` — changed `expires_at >= now` to `expires_at > now` in `is_valid`
- `tests/test_session.py` — added `test_token_expiring_at_boundary` reproducing the bug
## Evidence
- Baseline: `pytest tests/ -q` before changes → 214 passed (no pre-existing failures)
- Repro: new test on unmodified code → `FAILED tests/test_session.py::test_token_expiring_at_boundary — AssertionError: expected invalid`
- Fix verified: `pytest tests/test_session.py -q` → 31 passed
- Full suite: `pytest tests/ -q` → 215 passed
- Callers audited: `grep -rn "is_valid" --include="*.py"` → 3 call sites
(middleware.py:88, admin/views.py:41, tasks/cleanup.py:19); all treat a False
return as "reject/clean up", so the stricter boundary is safe for all.
- Lint/type: `ruff check .` clean; `mypy auth/` clean.
## Assumptions & decisions
- Treated "expires exactly now" as expired, matching the RFC 7519 `exp` claim
semantics referenced in `auth/README.md`.
## Risks & out of scope
- `tasks/cleanup.py` will now purge boundary sessions one tick earlier; behavior
change is negligible but noted.
- Did not touch refresh-token expiry (`auth/refresh.py`), which has the same
inclusive comparison — flagged as follow-up rather than expanding scope.
## Suggested follow-ups
- Apply the same boundary fix + test to `RefreshToken.is_valid`.
Example 2 — Blocked, reported honestly
## Summary
Attempted to upgrade `libfoo` 2.x → 3.x. Upgrade compiles, but 3.x removed the
streaming API used in `ingest/reader.py` and the replacement requires an async
runtime this codebase doesn't use. Stopping for a design decision rather than
introducing an event loop unilaterally. Status: ❌ blocked (no changes committed;
tree restored to baseline).
## Changes
None retained. Exploratory branch `spike/libfoo-3` contains the partial port
for reference.
## Evidence
- Baseline: `make test` on main → 96 passed
- After bump to 3.1.0: `make build` → 7 compile errors, all
`foo.stream_reader` removals (full list in spike branch commit message)
- Migration guide (libfoo CHANGELOG v3.0.0) confirms `stream_reader` was
replaced by async-only `foo.aio.reader`
- Verified rollback: `git status` clean on main; `make test` → 96 passed
## Assumptions & decisions
- Chose not to wrap the async API in `asyncio.run()` inside the hot ingest loop:
a quick benchmark on the spike branch showed ~4x slowdown on
`tests/perf/test_ingest_throughput.py`.
## Risks & out of scope
- Staying on 2.x: 2.19 receives security fixes until 2027-03 per upstream policy,
so there is no immediate pressure.
## Suggested follow-ups (decision needed)
1. Adopt async ingest properly (larger refactor, est. multi-day), or
2. Pin `libfoo <3` and revisit when the async migration is scheduled.
Recommendation: option 2 now, schedule option 1.
Example 3 — Complete with caveats
## Summary
Added CSV export to the reports page as requested. Status: ⚠️ complete with
caveats — feature works and is tested, but one pre-existing unrelated test
failure exists on main, and locale-specific number formatting is out of scope.
## Changes
- `reports/export.py` — new `to_csv(report)` using stdlib `csv` (no new deps)
- `reports/views.py` — added `/reports/<id>/export.csv` route, permission-gated
with the existing `can_view_report` check
- `tests/test_export.py` — 6 tests: happy path, empty report, unicode fields,
fields containing commas/quotes/newlines, permission denied
## Evidence
- Baseline: `pytest -q` on main → 1 pre-existing failure
(`test_email_digest_timezone`, also fails on untouched main — evidence: ran on
clean checkout of `main@a1b2c3d`)
- After change: `pytest -q` → same single pre-existing failure; all 6 new tests pass
- Injection edge case: verified fields starting with `=` are prefixed per OWASP
CSV-injection guidance; covered by `test_formula_injection_escaped`
- `ruff check .` and `mypy reports/` clean
## Assumptions & decisions
- Assumed UTF-8 with BOM for Excel compatibility, matching the existing XLSX
exporter's comment in `reports/xlsx.py:12`. Easy to change if undesired.
## Risks & out of scope
- Numbers are exported in machine format (`1234.5`), not locale format —
not requested, noted for the reviewer.
- Pre-existing `test_email_digest_timezone` failure is unrelated and untouched.
1---2name: safe-coding-workflow3description: A disciplined workflow for planning, executing, and verifying non-trivial code changes safely. Use this skill whenever the user asks for a code change that is hard, risky, multi-file, or ambiguous — including bug fixes, refactors, feature additions, dependency upgrades, migrations, performance work, or "fix this failing test" requests. Also use it when entering an unfamiliar codebase, when the blast radius of a change is unclear, or when the user asks you to "be careful", "don't break anything", or to explain what you changed and why. Do NOT skip this skill just because the change looks small — small diffs in unfamiliar code are where regressions hide.4---56# Safe Coding Workflow78A skill that turns "make this change" into a sequence of observable, verifiable9steps: inspect → plan → change minimally → verify → report with evidence.1011Core principle: **never claim something works that you have not observed12working.** Every claim in the final report must be backed by a command you ran,13output you read, or a file you inspected. Reasoning is expressed as visible14artifacts (notes, plans, command output), not as private deliberation.1516---1718## When to use1920Use this workflow when ANY of the following are true:2122- The task touches more than one file, or you don't yet know how many files it touches.23- You are new to this codebase or this area of it.24- The change affects public APIs, data models, persistence, auth, money, concurrency, or anything user-facing.25- The task description is ambiguous, contradictory, or missing acceptance criteria.26- Tests exist and could break, or tests don't exist and should.27- The user asked for a "quick fix" in code you haven't read yet. (Especially then.)2829You may abbreviate the workflow (not skip verification) only when the change is30a single-file, single-purpose edit in code you have already read this session,31with a fast test or run command available.3233---3435## Step-by-step workflow3637### Phase 0 — Restate the task3839Before touching anything, write down (in your working notes or response):40411. **Goal**: one sentence describing the desired end state.422. **Acceptance criteria**: how you will know it's done (specific behaviors, passing tests, output).433. **Explicit constraints**: anything the user said not to do (no new deps, keep API stable, etc.).444. **Open questions**: anything ambiguous. Decide per the "missing information" rules below whether to ask or proceed with a stated assumption.4546If you cannot state acceptance criteria, you are not ready to edit code.4748### Phase 1 — Inspect before editing4950Never edit a file you haven't read. Never assume structure you haven't verified.5152Run a reconnaissance pass and record what you find:5354```55# Layout and entry points56ls / tree (2 levels), README, docs/5758# Project type, dependencies, versions59package.json / pyproject.toml / go.mod / Cargo.toml / pom.xml ...60lockfiles (to see what's actually pinned)6162# How the project is built, run, and tested63Makefile, scripts section, CI config (.github/workflows, .gitlab-ci.yml)6465# Conventions and constraints66linter/formatter configs, tsconfig/mypy strictness, CONTRIBUTING.md, .editorconfig6768# State of the tree69git status, git log --oneline -10 (am I on a dirty tree? recent related changes?)70```7172Then narrow to the task area:7374- Locate the code paths involved (grep for the function/route/error message).75- Read the relevant files fully, not just the matching lines.76- Find the callers and callees of anything you plan to change (`grep -rn`, IDE-equivalent search). List them.77- Find existing tests for this area and run them **before** changing anything, so you know the baseline. A test that was already failing is not your regression — but you must know that before you edit.7879Record a short written map: "The request flows A → B → C; the bug is likely in B because ...; B is called from X and Y."8081### Phase 2 — Decompose into safe steps8283Break the task into steps where each step:8485- Has a single purpose (one behavior change, one refactor, one dependency bump — never mixed).86- Leaves the project in a working or at least buildable state.87- Is independently verifiable (a command that proves it worked).88- Is reversible (small enough to revert without archaeology).8990Order steps risk-first when possible: do the investigation and scaffolding (add a failing test that reproduces the bug) before the fix, and do behavior-preserving refactors in separate steps from behavior changes.9192Write the plan down as a numbered list with a verification command per step. Example:9394```951. Add failing test reproducing #142 (verify: pytest tests/test_auth.py -k expired — fails)962. Fix token expiry comparison in auth/session.py (verify: same test passes)973. Run full auth test suite (verify: pytest tests/test_auth.py — all pass)984. Run linter + typecheck (verify: ruff check, mypy — clean)99```100101### Phase 3 — Detect edge cases and hidden risks102103Before implementing, explicitly check the task against this list and write down which apply:104105- **Inputs**: empty, null/None, zero, negative, huge, malformed, wrong encoding, unicode, duplicates.106- **Boundaries**: off-by-one, inclusive vs exclusive ranges, first/last element, empty collections.107- **Time**: timezones, DST, clock skew, expiry exactly at boundary, leap days, ordering of async events.108- **Concurrency**: shared state, race on read-modify-write, idempotency of retries.109- **Persistence & migration**: old data shaped by the previous code; will existing rows/files still parse?110- **Compatibility**: public API signatures, serialized formats, config keys, CLI flags other code may depend on.111- **Error paths**: what happens when the dependency call fails, times out, returns partial data?112- **Security**: injection via the values you're now handling, secrets in logs, authz checks bypassed by a new code path.113- **Hidden callers**: dynamic dispatch, reflection, string-based imports, templates, scheduled jobs, other repos.114115For each applicable risk, either handle it in the plan, cover it with a test, or explicitly note it as out of scope in the report.116117### Phase 4 — Implement with safe editing rules118119(See "Safe code editing rules" below.) Work step by step through the plan, running the step's verification command before moving on. If a step's verification fails, stop and fix or replan — do not stack changes on a broken state.120121### Phase 5 — Verify the whole122123Run the full verification checklist (below). Verification of individual steps is not sufficient; interactions between steps are where regressions live.124125### Phase 6 — Report with evidence126127Produce a final report in the reporting format (below). Every claim of success must cite the command and its observed result.128129---130131## Safe code editing rules1321331. **Read before write.** Read the entire function/class you're editing and its immediate neighbors. Never edit from a grep snippet alone.1342. **Minimal diff.** Change only what the task requires. No drive-by reformatting, renaming, or "improvements" outside scope — they pollute review and hide the real change. If you notice unrelated problems, record them in the report instead of fixing them silently.1353. **Match existing conventions.** Follow the project's style, naming, error-handling patterns, and test structure, even where you'd personally choose differently.1364. **Preserve public contracts.** Don't change function signatures, API shapes, config keys, serialized formats, or exported names unless the task explicitly requires it. If you must, list every caller you updated and how you found them.1375. **No silent behavior changes.** If a fix necessarily changes observable behavior beyond the bug (error messages, ordering, defaults), call it out explicitly.1386. **No new dependencies without justification.** Prefer the standard library and existing project deps. If a new dependency is genuinely needed, state why, check its license and maintenance status, and pin the version.1397. **Never weaken safety to make things pass.** Do not delete or skip failing tests, loosen assertions, broaden `except`/`catch` blocks, disable lint rules, or add `# type: ignore` to silence errors — unless the test/rule itself is demonstrably wrong, in which case say so in the report with reasoning.1408. **No destructive operations without an explicit go-ahead.** Do not delete files/branches, run migrations against shared databases, force-push, rewrite history, or touch production systems or credentials on your own initiative.1419. **Keep the tree recoverable.** Work on a clean tree where possible; commit or checkpoint at safe boundaries so any step can be reverted alone.14210. **Handle generated and vendored code correctly.** Never hand-edit generated files (protobufs, lockfiles, build output); change the source and regenerate.14311. **Secrets stay out.** Never hardcode credentials, tokens, or user data in code, tests, fixtures, or logs.14412. **When a "simple fix" grows**, stop. If the diff is spreading beyond the plan, return to Phase 2 and replan rather than improvising.145146---147148## Deciding what to do next when information is missing149150Classify the gap, then act:151152| Gap type | Action |153|---|---|154| Answer exists in the repo (behavior, conventions, structure) | **Investigate.** Read code, run it, write a scratch test. Don't ask the user things the codebase can answer. |155| Answer exists in docs of a dependency/tool | **Look it up** (docs, changelog, source in the lockfile's pinned version). |156| Product/intent decision (which behavior is *desired*), destructive or irreversible action, scope expansion, security tradeoff | **Ask the user.** Present the options you see and your recommendation. |157| Low-stakes ambiguity where any reasonable reading is fine | **Proceed with a stated assumption.** Write the assumption in your plan and repeat it prominently in the final report so it can be corrected cheaply. |158| Blocked entirely (missing access, broken environment, cannot reproduce) | **Stop and report** exactly what you tried, what failed, and what you need — do not fabricate a workaround that pretends the blocker is gone. |159160Rule of thumb: investigate facts, ask about intent, state assumptions you make, and never let an unstated assumption reach the final report.161162---163164## Verification checklist165166Do not claim completion until every applicable item is checked, each backed by an actually executed command and observed output:167168- [ ] **Baseline recorded**: pre-change test/build status was captured, so new failures are distinguishable from pre-existing ones.169- [ ] **The change is exercised**: at least one test or manual run demonstrably executes the new/changed code path (not just "tests pass" — tests that never touch your code prove nothing).170- [ ] **New/updated tests fail without the fix** (for bug fixes: you saw the test fail on the old code, then pass on the new).171- [ ] **Relevant test suite passes**: run the project's own test command (from CI config or scripts), not a guessed one.172- [ ] **Build/compile/typecheck passes** with the project's real configuration.173- [ ] **Linter/formatter passes**, using the project's config.174- [ ] **Edge cases from Phase 3** are covered by tests or explicitly noted as out of scope.175- [ ] **Diff reviewed**: read the full `git diff` yourself; confirm no unintended files, debug prints, commented-out code, TODO placeholders, or secrets.176- [ ] **Callers audited**: every caller of changed interfaces was found and updated or confirmed unaffected.177- [ ] **No verification theater**: no skipped/disabled tests, loosened assertions, or suppressed warnings introduced to make checks green.178- [ ] **Pre-existing failures listed**: anything that was already broken before your change is documented as such, with evidence of the baseline run.179180If an item cannot be verified in this environment (e.g., no test runner available), the report must say so explicitly rather than implying it passed.181182---183184## When the project has no test suite185186Many real projects — scripts, document pipelines, agent systems, internal tools, prototypes — have no pytest/jest suite, no typechecker, and no linter config. The core rule does not relax: **never claim something works that you have not observed working.** Only the instruments change. Substitute these, cheapest first, and write the chosen command into the plan exactly as you would a test:1871881. **Load check** — proves the code still parses and imports, in about a second:189 `python3 -c "import pkg.module"` · `node --check file.js` · `bash -n script.sh` · `tsc --noEmit` · `jq empty config.json`1902. **Smoke run** — start the thing and touch it once: run the CLI with `--help`, `curl` the health endpoint and read the status code, open the page. Record the actual output line, not "it ran".1913. **Targeted end-to-end script** — for UI/browser/API work, a short script that drives the real thing (Playwright, curl, a scratch client) and prints explicit pass/fail per assertion. **Keep the script** — it is the regression test the project didn't have, and the next change gets it for free.1924. **Before/after evidence on the same command** — screenshot, JSON output, or diff of identical invocations run before and after the change. This is how you get a baseline when there are no tests to run.1935. **Verify at the destination** — if the change ships (deploy, sync, publish), check there too: local success is not deployment success.1946. **Verify the delivery step itself** — after `git push`/deploy, confirm the remote state actually moved (`git status -sb` showing no unpushed commits, a fetched health endpoint, the file visible at the destination). Reporting "pushed" because the command exited 0 without checking the resulting state is the same failure class as claiming a test passed without running it.195196Two things that look like verification but are not: *reading the diff and finding it correct* (that is review, not execution), and *the tool reporting success* (an editor writing a file proves the write, not the behavior).197198If none of these are possible, say so plainly in the report — "no way to execute this here; unverified" — and never let an unverified claim sit in a Summary line.199200---201202## Reporting format203204Final reports use this structure:205206```markdown207## Summary208One paragraph: what was asked, what was done, current status209(✅ complete / ⚠️ complete with caveats / ❌ blocked).210211## Changes212- `path/to/file.py` — what changed and why (one line per file)213214## Evidence215For each verification claim: the command run and the observed result.216Quote the decisive lines of output, not walls of logs.217218## Assumptions & decisions219Assumptions made where information was missing; alternatives considered220and why the chosen approach won.221222## Risks & out of scope223Known limitations, edge cases deliberately not handled, pre-existing224issues noticed but not fixed, and anything the reviewer should look at225extra carefully.226227## Suggested follow-ups (optional)228Improvements observed but intentionally not made.229```230231Rules for reports:232233- Every "passes" / "works" / "fixed" claim links to an Evidence entry.234- Distinguish clearly between *verified* ("ran X, saw Y") and *expected* ("should also work for Z, not tested here").235- Report failures and partial completion honestly and prominently — a precise "blocked" report is a success; a vague "done" that isn't is a failure.236237---238239## Examples of good final reports240241### Example 1 — Bug fix, fully verified242243```markdown244## Summary245Fixed #142: sessions with tokens expiring exactly at the request timestamp were246accepted instead of rejected. Root cause was an inclusive comparison in247`Session.is_valid`. Status: ✅ complete.248249## Changes250- `auth/session.py` — changed `expires_at >= now` to `expires_at > now` in `is_valid`251- `tests/test_session.py` — added `test_token_expiring_at_boundary` reproducing the bug252253## Evidence254- Baseline: `pytest tests/ -q` before changes → 214 passed (no pre-existing failures)255- Repro: new test on unmodified code → `FAILED tests/test_session.py::test_token_expiring_at_boundary — AssertionError: expected invalid`256- Fix verified: `pytest tests/test_session.py -q` → 31 passed257- Full suite: `pytest tests/ -q` → 215 passed258- Callers audited: `grep -rn "is_valid" --include="*.py"` → 3 call sites259 (middleware.py:88, admin/views.py:41, tasks/cleanup.py:19); all treat a False260 return as "reject/clean up", so the stricter boundary is safe for all.261- Lint/type: `ruff check .` clean; `mypy auth/` clean.262263## Assumptions & decisions264- Treated "expires exactly now" as expired, matching the RFC 7519 `exp` claim265 semantics referenced in `auth/README.md`.266267## Risks & out of scope268- `tasks/cleanup.py` will now purge boundary sessions one tick earlier; behavior269 change is negligible but noted.270- Did not touch refresh-token expiry (`auth/refresh.py`), which has the same271 inclusive comparison — flagged as follow-up rather than expanding scope.272273## Suggested follow-ups274- Apply the same boundary fix + test to `RefreshToken.is_valid`.275```276277### Example 2 — Blocked, reported honestly278279```markdown280## Summary281Attempted to upgrade `libfoo` 2.x → 3.x. Upgrade compiles, but 3.x removed the282streaming API used in `ingest/reader.py` and the replacement requires an async283runtime this codebase doesn't use. Stopping for a design decision rather than284introducing an event loop unilaterally. Status: ❌ blocked (no changes committed;285tree restored to baseline).286287## Changes288None retained. Exploratory branch `spike/libfoo-3` contains the partial port289for reference.290291## Evidence292- Baseline: `make test` on main → 96 passed293- After bump to 3.1.0: `make build` → 7 compile errors, all294 `foo.stream_reader` removals (full list in spike branch commit message)295- Migration guide (libfoo CHANGELOG v3.0.0) confirms `stream_reader` was296 replaced by async-only `foo.aio.reader`297- Verified rollback: `git status` clean on main; `make test` → 96 passed298299## Assumptions & decisions300- Chose not to wrap the async API in `asyncio.run()` inside the hot ingest loop:301 a quick benchmark on the spike branch showed ~4x slowdown on302 `tests/perf/test_ingest_throughput.py`.303304## Risks & out of scope305- Staying on 2.x: 2.19 receives security fixes until 2027-03 per upstream policy,306 so there is no immediate pressure.307308## Suggested follow-ups (decision needed)3091. Adopt async ingest properly (larger refactor, est. multi-day), or3102. Pin `libfoo <3` and revisit when the async migration is scheduled.311Recommendation: option 2 now, schedule option 1.312```313314### Example 3 — Complete with caveats315316```markdown317## Summary318Added CSV export to the reports page as requested. Status: ⚠️ complete with319caveats — feature works and is tested, but one pre-existing unrelated test320failure exists on main, and locale-specific number formatting is out of scope.321322## Changes323- `reports/export.py` — new `to_csv(report)` using stdlib `csv` (no new deps)324- `reports/views.py` — added `/reports/<id>/export.csv` route, permission-gated325 with the existing `can_view_report` check326- `tests/test_export.py` — 6 tests: happy path, empty report, unicode fields,327 fields containing commas/quotes/newlines, permission denied328329## Evidence330- Baseline: `pytest -q` on main → 1 pre-existing failure331 (`test_email_digest_timezone`, also fails on untouched main — evidence: ran on332 clean checkout of `main@a1b2c3d`)333- After change: `pytest -q` → same single pre-existing failure; all 6 new tests pass334- Injection edge case: verified fields starting with `=` are prefixed per OWASP335 CSV-injection guidance; covered by `test_formula_injection_escaped`336- `ruff check .` and `mypy reports/` clean337338## Assumptions & decisions339- Assumed UTF-8 with BOM for Excel compatibility, matching the existing XLSX340 exporter's comment in `reports/xlsx.py:12`. Easy to change if undesired.341342## Risks & out of scope343- Numbers are exported in machine format (`1234.5`), not locale format —344 not requested, noted for the reviewer.345- Pre-existing `test_email_digest_timezone` failure is unrelated and untouched.346```
Run npx skillmds@latest add dga-sd/safe-coding-workflow in your terminal (requires Node.js), paste this page's agent-chat prompt into Claude, Cursor, or any MCP-connected agent, or download the SKILL.md file and copy it into your agent's skills directory.
A disciplined workflow for planning, executing, and verifying non-trivial code changes safely. Use this skill whenever the user asks for a code change that is hard, risky, multi-file, or ambiguous — including bug fixes, refactors, feature additions, dependency upgrades, migrations, performance work, or "fix this failing test" requests. Also use it when entering an unfamiliar codebase, when the blast radius of a change is unclear, or when the user asks you to "be careful", "don't break anything", or to explain what you changed and why. Do NOT skip this skill just because the change looks small — small diffs in unfamiliar code are where regressions hide. It is listed under Productivity on SkillMD.
This skill has not completed SkillMD's automated safety review yet. Capability flags: makes network calls, reads secrets. SkillMD never runs a skill's scripts for you; review the SKILL.md before installing.
This skill is tagged as working with Claude Code, Claude.ai, OpenAI Codex. SKILL.md is an open format, so most agents that read a skills directory can load it too.
Yes. Installing skills from SkillMD is free, and the skill stays under its author's original license.
DGA-SD (@dga-sd) published this skill. Their other Agent Skills are listed on their SkillMD profile.