# Debug Mode

> Evidence-driven runtime debugging loop. Generates competing hypotheses, instruments the code with session-tagged probes that stream to an isolated local log sink, has the user reproduce the bug, then confirms or rejects each hypothesis from real runtime data before fixing and removing every probe. Use when a bug's cause is not obvious from reading the code, when a fix attempt has already failed, for heisenbugs, race conditions, state corruption, "works locally but not in prod", or whenever the user says debug mode, find the root cause, or add logging to figure this out.

- Skill: `heiets/debug-mode` (Agent Skill, multi-file: 4 files)
- Install (CLI): `npx skillmds@latest add heiets/debug-mode`
- Raw SKILL.md: https://api.skillmd.com/api/skills/heiets/debug-mode/raw
- Safety review: pending (external: skill-scanner PASS, skillspector PASS)
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: heiets (https://skillmd.com/u/heiets)
- Updated: 2026-09-22
- Page: https://skillmd.com/skills/heiets/debug-mode

---


# Debug Mode

Most debugging fails because the agent guesses from source code and ships a plausible fix that does not address the real cause. This skill forbids that. **No fix may be proposed until runtime evidence has confirmed a specific hypothesis.**

The loop: **hypothesize → instrument → reproduce → read evidence → fix → verify → clean up.**

Everything runs through one dependency-free CLI:

```
scripts/debugctl.py
```

Set `DBG="python3 <abs path to>/scripts/debugctl.py"` once and reuse it. Run it from inside the project being debugged.

## Non-negotiables

1. **Hypotheses before probes.** A probe exists to discriminate between competing explanations. If you cannot say which hypothesis a probe kills, do not add it.
2. **Never guess from source once instrumented.** If zero logs arrived, run `doctor` and fix the pipeline. Do not fall back to reading code and calling it a root cause.
3. **The user reproduces, not you** — unless they have handed you a command that triggers the bug. Stop and wait at the repro gate.
4. **A rejected hypothesis is a result.** Record it. Do not quietly drop it.
5. **The session is not done until `verify-clean` exits 0.** Leaving probes in the tree is the single most common failure of this style of debugging.
6. **Probes must be inert**: never block, never throw, never mutate program state, never swallow an exception belonging to the code under test.

## Phase 0 — Open an isolated session

```bash
$DBG start --goal "<one-line bug description>"
```

Prints a **session id**, a **sentinel** (e.g. `DBG_A1B2C3D4`), an **ingest URL**, and a **log file**. Everything is scoped to `.debugsessions/<session-id>/` and auto-gitignored. Concurrent sessions get separate ports, separate logs, and separate sentinels, so they never interfere.

Flags that matter:

| Situation | Flag |
|---|---|
| Android emulator | `--host 10.0.2.2` |
| Docker / compose | `--host host.docker.internal` |
| Physical device | `--host <your LAN IP>` |
| No network, sandbox, or bind denied | `--no-server` (file sink) |

If `start` reports that listening sockets are forbidden, re-run with `--no-server` — the rest of the loop is identical.

## Phase 1 — Generate competing hypotheses

Read the relevant code first. Then record **3–5 hypotheses that disagree with each other**. One hypothesis is not a hypothesis, it is a hunch.

```bash
$DBG hypo add "<what is actually wrong>" --signal "<the log line that would confirm or reject this>"
```

The `--signal` is the contract. Writing it forces the probe design: if you cannot name the observation that would settle the question, the hypothesis is too vague to test.

Aim for hypotheses at different layers — bad input, wrong branch taken, wrong order of operations, stale or shared state, boundary/type coercion, async timing. See `references/hypotheses.md`.

## Phase 2 — Instrument

Get a correct emitter for the language, already carrying the right URL and sentinel:

```bash
$DBG snippet python              # with notes and a usage example
$DBG snippet python --helper     # just the helper block, safe to pipe into a file
```

Languages: `python node browser typescript go java kotlin swift ruby php rust csharp c cpp shell dart elixir file`, plus aliases (`js`, `ts`, `react`, `android`, `ios`, `flutter`, `bash`, `golang`, `dotnet`, …).

Read the NOTE the snippet prints — several runtimes send detached, and a short-lived process can exit before the probe lands.

Paste the helper once, then place one probe per hypothesis **at the decision point that discriminates it**. Rules:

- Every probe line **must contain the sentinel** — that is how cleanup finds it. Multi-line helpers go between `<SENTINEL>:begin` and `<SENTINEL>:end`.
- **Put every probe on its own line.** Cleanup deletes sentinel-bearing lines *in full*, so a probe appended to a line that also holds program logic would take that logic with it. Never do this:

  ```js
  if (user.plan === "free") { return (_dbg("H1","b",{x}),"basic"); } // DBG_XXXX  ← deletes the branch
  ```

  Expand the block and give the probe its own line instead. `cleanup` refuses (exit 3) when it detects this and leaves the file untouched, but it is far cheaper to avoid.
- Tag every probe with its hypothesis id (`H1`, `H2`, …).
- Log **values, not prose**: the actual variable, the branch taken, the length, the id, the timestamp. `"got here"` proves nothing.
- Capture both sides of a comparison — the expected and the actual.
- Probe the boundary *and* the interior, so a silent probe tells you the code never reached it.

Then record what you touched:

```bash
$DBG instrumented    # lists every line carrying this session's sentinel
```

## Phase 3 — The reproduce gate

Write a marker so this attempt's evidence is separable from the last:

```bash
$DBG mark "repro-1"
```

Then **give the user exact, numbered reproduction steps and stop.** Tell them what you expect to see. Wait.

### The two actions that drive every cycle

The user comes back with exactly one of these:

**A. "The issue reproduced"** → the bug happened, evidence was captured.

```bash
$DBG reproduced --note "<what the user observed>"
```

Go to Phase 4.

**B. "The issue is fixed"** → the bug no longer happens.

```bash
$DBG fixed --note "<what the user observed>"
```

Go to Phase 6 — cleanup. **This is a terminal action: stop investigating and remove the instrumentation.**

If `reproduced` reports **0 log entries**, the probes never reached the sink. Run `$DBG doctor` and repair the pipeline before drawing any conclusion.

## Phase 4 — Read the evidence

```bash
$DBG summary                      # which probes fired, which stayed silent
$DBG logs --since-last-mark       # only this repro attempt
$DBG logs --hypothesis H2         # evidence for one hypothesis
$DBG logs --grep "user_id|null"   # regex across the payload
```

`summary` lists **silent hypotheses** — probes that never fired. A silent probe is evidence: that code path did not execute. That frequently *is* the bug.

Adjudicate every hypothesis, quoting the actual log values:

```bash
$DBG hypo set H1 confirmed  --evidence "tax_base=25.0 == subtotal while discounted=22.5"
$DBG hypo set H2 rejected   --evidence "apply_discount returned 22.5, the correct total"
$DBG hypo set H3 inconclusive --evidence "probe never fired; path not reached"
```

**If every hypothesis is rejected, that is progress, not failure.** You have eliminated the obvious. Form a new round from what the logs *did* show, add probes, and return to Phase 3. Do not start guessing.

## Phase 5 — Root cause and targeted fix

State the root cause in one sentence, grounded in a specific log line. Then make **the smallest change that addresses that cause.** Do not refactor, do not fix adjacent things you noticed, do not "improve" the code while you are in there.

Leave the instrumentation in place — you need it to verify.

Mark a fresh attempt and hand the user verification steps:

```bash
$DBG mark "verify-1"
```

Then wait for one of the two actions again. If the bug still reproduces, the logs now tell you why the fix missed; iterate.

## Phase 6 — Clean up (mandatory)

```bash
$DBG cleanup              # dry run: exactly what will be removed
$DBG cleanup --apply      # removes only THIS session's sentinel lines
$DBG verify-clean         # exits 0 only if the tree is clean
$DBG end                  # refuses to close while probes remain
```

`cleanup` backs up every modified file under the session directory first. It matches only this session's sentinel, so a concurrent debug session's probes are untouched.

Exit codes: `cleanup` returns **3** and changes nothing if any probe is mixed into a line of real code — hand-edit those lines to separate the probe, then re-run. `verify-clean` returns **2** while any probe remains, **0** when the tree is clean.

**Read the final diff before reporting done.** `verify-clean` proves the sentinel is gone; it does not prove the surrounding code is intact. Confirm the diff contains your fix and nothing else.

Finish by reporting: the root cause, the evidence that proved it, the hypotheses you rejected, and the diff you are shipping. Confirm `verify-clean` passed.

## Command reference

| Command | Purpose |
|---|---|
| `start --goal "..."` | open an isolated session |
| `status` / `list` | current state / all sessions |
| `snippet <lang> [--helper]` | ready-to-paste probe for that language |
| `hypo add/set/list` | the hypothesis ledger |
| `mark <label>` | split one repro attempt from the next |
| `reproduced` / `fixed` | the two cycle actions |
| `logs` / `summary` | read and aggregate evidence |
| `instrumented` | list every probe in the tree |
| `doctor` | why no logs are arriving |
| `cleanup --apply` / `verify-clean` | remove probes, prove it |
| `stop [--all]` / `end` | stop one sink (or every one in the project) / close the session |

Global flags work on either side of the subcommand: `--session <id>` targets a specific session, `--json` gives machine-readable output, `--root <dir>` sets the project root.

## When logs do not arrive

Run `$DBG doctor` first — it checks the sink, the health endpoint, the capture count, and whether probes are present, then prints targeted hints. Full matrix in `references/transports.md`.

The usual culprits: the app was never restarted after instrumenting; a browser page on `https` blocked from posting to an `http` sink; an Android emulator dialing `127.0.0.1` (itself) instead of `10.0.2.2`; a container with no route to the host; or a process too short-lived for an async send to land — use `--no-server` and the file sink for that last one.

## Further reading

- `references/hypotheses.md` — how to generate hypotheses that actually discriminate
- `references/instrumentation.md` — probe placement patterns, and what to log for races, state corruption, and performance
- `references/transports.md` — getting logs out of browsers, emulators, containers, remote hosts, and CI

