# Comment Audit

> The standard for code comments, both writing them and removing them. Use when writing or reviewing a comment, when asked to remove unnecessary, obvious, or useless comments, clean up comment noise, or audit a file or codebase for comment quality, and before committing a change that added comments.

- Skill: `aayushbtw/comment-audit` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add aayushbtw/comment-audit`
- Raw SKILL.md: https://api.skillmd.com/api/skills/aayushbtw/comment-audit/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Security
- Author: aayushbtw (https://skillmd.com/u/aayushbtw)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/aayushbtw/comment-audit

---


# Comments

The default is no comment. The code says what it does; a comment is for what the code cannot say. Every rule below is that one rule applied twice: once when deciding whether to write a line, once when deciding whether to keep it.

## Never write

Five reasons a comment fails, and a report names the one it fell under.

**It duplicates the code.**

- **The obvious**: Anything recoverable by reading the line. `// set the title`, `// loop over items`, `// return the result`, `// YYYY-MM-DD` above a `.slice(0, 10)`. When the comment exists because the name is bad, the fix is the rename, so flag it rather than deleting the only explanation.
- **What the types already say**: `@param userId The user id`. Document what the type cannot: units, ownership, what `null` means, the format of a string.

**It belongs somewhere else.**

- **History**: "was sized for English", "had drifted", "used to be a utility", and diff narration like "new", "updated", "moved from utils". How the code got here is the commit message's job. The exception is a fact that stops the obvious wrong edit, and it survives only in present tense. "The columns are `rated_at` and `message_agent`, not the `timestamp`/`agent` they look like" earns its line; "#217 renamed these" does not say the same thing to someone who was not there.
- **Design rationale at length**: A paragraph arguing for a decision belongs in the PR description or a design doc. The sentence naming the constraint can stay.
- **Attribution**: No "added by", no tool signatures.

**It says nothing.**

- **The contentless**: `// helper function`, `// important`, `// magic`, `// handle the edge case` without saying which edge case. A docstring written to satisfy a coverage rule lands here too; "the file has some" is not a reason.
- **Hedges and apologies**: `// not sure if this is right`, `// hacky`, `// sorry`. Either fix it or state the concrete problem.
- **Futures without a condition**: `// could optimize later`. A removal note needs a condition it can be checked against, or it is noise forever.

**It will not survive the next edit.**

- **A fact that lives somewhere else**: A number, a threshold, a default, or a behaviour described in prose while the real one sits in another file. Nothing links the two, so the day that file changes this line becomes a lie, and it reads as authoritative the whole time. Say why the value is what it is, which stays true, rather than what it is, which will not. Writing this way is free; finding one that has already gone stale means reading the other file, so an audit only catches these on the claim pass.

**It isn't prose.**

- **Commented-out code**: Delete it. Git has it.
- **Section banners and dividers**: `// ===== helpers =====`. If a file needs signposting it needs splitting. A folding marker (`//#region`, `// <editor-fold>`) is the same thing with editor support, so it is evidence of the same problem: flag it rather than deleting it, because the marker is someone's deliberate structure and the fix is splitting the file. Where the language makes it a real compiler directive rather than an editor convention, it belongs in **Never delete** instead.

## Write only when

- **The *why* is unrecoverable from the code**: A workaround, a deliberate deviation from a library default, an ordering that looks arbitrary and isn't.
- **Something load-bearing looks deletable**: A `display: contents`, a ref, a guard, a `margin: 0` undoing a vendored rule, keyframes holding one value, a constant duplicated from a token the build can't expose. This one is wanted, not merely allowed: it sits where the person about to delete the line will see it.
- **Dead or transitional code needs a removal condition**: "delete once X ships". A condition, not a wish.
- **A public contract isn't in the types**: Units, ownership, what `null` means, a worked example of the output (`/** "March 27, 2026" */`).
- **Provenance on a vendored or forked file**: Where it came from, and what re-pulling would undo.

## Never delete

Some things are shaped like comments but are not comments. Something reads them, and deleting them changes behaviour, breaks a build, or has consequences outside the code.

**The test, applied before deleting anything:** would a tool read this line? A comment written for a machine does not read like a sentence to a person. It carries a tool's name, a directive verb, a colon or a rule id (`eslint-disable-next-line @typescript-eslint/no-explicit-any`), an `@` pragma, a `#!`, or a triple slash. If a line looks like that, treat it as code until proven otherwise. Grep the token across the repo's config and dependencies; if a linter, bundler, compiler, or coverage tool in the project answers to that name, leave the line alone.

The families this covers, as examples rather than an exhaustive list:

- **Linter and formatter directives**: `eslint-disable*`, `biome-ignore`, `oxlint-disable`, `prettier-ignore`, `deno-lint-ignore`, `# noqa`, `# ruff: noqa`, `# pylint: disable`, `# fmt: off`, `//nolint`, `// swiftlint:disable`. Each one re-enables a rule someone turned off deliberately, and the diff that re-breaks CI will not obviously point back here.
- **Compiler and type-checker pragmas**: `@ts-expect-error`, `@ts-ignore`, `@ts-nocheck`, `@ts-check`, `# type: ignore`, `/// <reference types="..." />`, `// @flow`, `/** @jsxImportSource */`. `@ts-expect-error` fails the build the moment it is removed, and a `<reference>` line silently drops types.
- **Build and bundler instructions**: `//go:build` and `// +build` tags, `//go:embed`, `/* webpackIgnore */`, `// webpackChunkName`, `// @vite-ignore`, and a `#!` shebang on line one.
- **Coverage, codegen, and tooling markers**: `/* istanbul ignore next */`, `// c8 ignore`, `// v8 ignore`, `// @generated`, `// cspell:disable`, `// language=SQL`.
- **Doc comments the toolchain publishes**: Rust `///` and `//!`, Python docstrings that feed `help()` or Sphinx. These are output, not annotation. Judge them as documentation, which mostly means trimming, not deleting.
- **Licence and copyright headers**: SPDX lines, `@license`, vendored notices. They restate nothing and look like pure boilerplate, which is exactly why an audit deletes them by accident. Removing one is a legal question, not a style one.
- **A deletable comment sitting on top of any of these**: Remove the offending line, not the run of comment lines around it. `// Close the connection` directly above a `// @ts-expect-error` looks like one two-line block, and taking the block breaks the build.

When you cannot tell whether something reads a line, keep it and flag it. A kept comment costs a line; a deleted directive costs a build, and the person who finds it will not be looking in the comment audit.

## Size

One line by default, and a one-line change gets at most a one-line comment. Past three lines, ask what the extra lines are doing: restatement, or a why that genuinely takes that long. A removal condition that has to name a deployment order, or a constraint whose reason is a two-step argument, is worth the lines and belongs where the code is. Padding is not, at any length.

"Match the surrounding density" means the file being edited, not the densest file in the repo, and it never justifies more than these rules allow.

## Auditing

A file, a diff, or a whole codebase all use the same rules. What changes is how much you look at before touching anything.

### Two passes

The passes stack. The local pass always runs; the claim pass adds to it and never replaces it.

**The local pass — the default.** A comment is judged against the comment itself and the lines it sits on, nothing further. Four of the five groups in **Never write** are recognisable that way, because each is a comparison against something already on screen: the code beneath it, or the comment's own emptiness. The fifth, a fact that lives somewhere else, needs that other file, so it waits for the claim pass. The cost is one read per file, and it does not grow with the size of the codebase.

The one thing that reaches outside those lines is the **Never delete** test, which greps a suspected directive against the project's config. That is deliberate. It is a single cheap lookup, and it is what stands between an audit and a broken build.

A path in a comment is the exception worth checking, because it is one filesystem lookup and paths rot every time a file moves. If the file is gone, the comment is stale: flag it, do not guess the new location. What the file *says* is a claim, and that waits for the claim pass.

A comment that asserts anything else you cannot check from those lines is left alone in this pass, whatever it looks like. Not deleted, not trimmed. Count it and report it, because that count is what tells the reader whether the claim pass is worth buying.

**The claim pass — opt in.** Verify what comments assert: recompute the arithmetic, trace the token, confirm the name still exists, check the default was not overridden. A stale comment and a comment that was always wrong surface here together, because telling them apart costs the same single read of the code the comment describes. This is where contradictions are found, and it is the expensive half, because each claim is its own investigation across files. Run it when the user asks for it, or on a scope small enough to be worth it, such as one file or one diff.

Say what the claim pass would cost before running it on a large codebase: it scales with the number of claim-bearing comments, not with the number of files.

### Scope

**One file or one diff** — read it, apply the rules, edit, then report. No survey step; the scope is already known, and it is small enough that the claim pass is usually worth running too.

**A codebase**:

1. Exclude generated and vendored files. Check `.gitignore`, formatter and linter `ignorePatterns`, and headers that say the file is generated (`*.gen.*`, `*-configuration.d.ts`).
2. Bound the work:
   ```bash
   rg -c -g '!node_modules' -g '*.{ts,tsx,js,jsx,css,py,go,rs,rb,sh,sql,lua,html}' \
      '(^\s*//|^\s*/\*|^\s*\*\s|^\s*#|^\s*--|\{/\*|<!--)' . | sort -t: -k2 -rn
   ```
   Adjust the pattern to the languages actually present. The default misses anything whose comment syntax is not in it, and a language you skip silently reports zero.
3. Read the top files and report the plan before editing: how many comments, in which categories, how many carry claims, and anything you would flag. A sweep across hundreds of lines is hard to review after the fact and cheap to redirect beforehand.
4. Apply the agreed changes, working files in that order, running whichever passes were agreed. Read every comment in its surrounding code; a comment is judged against the lines it sits on, never as a string.
5. Report per **Output**.

### The evidence rule

The claim pass has its own procedure, and it is the expensive half. Read `references/claim-pass.md` when running it. On a local pass, do not: leave every claim standing and count it.

### Where to look first

- **It duplicates the code** is the bulk of what an audit removes, and the fastest to judge: the answer is directly beneath the comment.
- **It belongs somewhere else** and **it isn't prose** need no code reading at all. Recognise the shape and move on.
- **It says nothing** is nearly as quick, but read the surrounding lines before cutting: `// handle the edge case` is contentless, while `// handle the edge case where the retry lands after the socket closes` is the reason the branch exists.
- A comment that reads as legitimate and is quietly wrong is the expensive one, and the local pass does not find it. That is the claim pass.

### Trim before deleting

Most bad comments are good comments that ran long. If one sentence carries the why and the rest is restatement, keep the sentence.

## Guardrails

- **Comments are the whole object**: Comment lines are the only thing an audit changes. Not the statements around them, not a rename it suggests, and never a doc, README, or `CLAUDE.md`. The skill is trusted because the change is narrow and mechanical; anything else is a separate job that needs a different review. Note what you find and report it.
- **An empty block whose only content is the comment**: Deleting it trips `no-empty` / `no-empty-function`. Leave it, or restructure deliberately.
- **Verify only what you changed**: If comments were deleted, confirm the files still pass whatever check the project already has, since an emptied block or a lost directive fails there and nowhere else. If nothing was deleted, there is nothing to verify and running the suite proves nothing about the audit. Never run a build or start a dev server.

## Output

Open with what was covered: which passes ran, files read, comment lines judged, and which paths were excluded as generated or vendored. A report that deletes nothing is a real result, but only if the reader can tell it apart from a shallow pass, and only the denominators do that.

Then report in this order:

1. **Flagged** — comments the code contradicts, stale references, anything you were unsure about. This is the most valuable output; never silently resolve one.
2. **Deleted** — grouped by the **Never write** category they fall under.
3. **Trimmed.**
4. **Verified and kept** — comments whose claim cost real work to confirm: arithmetic you recomputed, a token you traced, a name you checked still exists. This is what separates a clean codebase from an unexamined one, and it is the only evidence a zero-deletion result is honest.
5. **Claims not verified** — on a local pass, the comments left standing because checking them needs more than the lines they sit on. Give the count and where they cluster, so the user can ask for the claim pass on the part that matters instead of the whole repo.
6. **Worth a comment** — a line whose *why* is unrecoverable and undocumented, found while reading. Name it and move on. An audit removes and trims; writing the missing comment needs the author's knowledge, not the auditor's guess.

