An Agent Skill that stops your coding agent from writing comments, and makes it write code that does not need them.
Same information, none of it in prose next to the code. Names, types, tests and the PR carry it instead.
Two steps. The first installs the Skill, the second makes it always on.
# 1. the Skill - the full guidance, loaded on demand
npx skills@latest add ctxr-dev/no-comments
# 2. the Rule - makes it the default for every message
mkdir -p ~/.claude/rules
curl -fsSL https://raw.githubusercontent.com/ctxr-dev/no-comments/main/rules/no-comments.md \
-o ~/.claude/rules/no-comments.mdRestart Claude Code. Your agent applies it on its own from then on.
Why two steps. Skills load on demand, so the agent reads the description and decides. Rules load every session, with no decision involved. A comment ban only works if it is loaded before the agent writes its first line, so the Rule is the part that matters here. The Skill is what the agent reads when it wants the worked examples. The skills CLI installs Skills, so it cannot place a rule file for you.
Other install options
Global — add -g to the skills command to install for all your projects.
Project-level rule — put the rule at .claude/rules/no-comments.md in your repo root, or paste its contents into CLAUDE.md.
Other agents — Codex, omp, Cursor, Copilot, Gemini CLI, Windsurf and OpenCode each get a ready-made block in the next section.
From a local clone — npx skills@latest add . and cp rules/no-comments.md ~/.claude/rules/no-comments.md. Use this until the repo is published.
Windows PowerShell — use $env:USERPROFILE\.claude\rules in place of ~/.claude/rules.
Track the repo instead of copying — ln -sf "$PWD/rules/no-comments.md" ~/.claude/rules/no-comments.md
Try it without installing — npx skills use ctxr-dev/no-comments | claude
Inspect first — npx skills add ctxr-dev/no-comments --list
Install the Rule in another agent — Codex, omp, Cursor, Copilot, Gemini CLI, Windsurf, OpenCode
Every block installs the same file: rules/no-comments.md. Run the one for your agent once. All of them are user-global unless the comment says project.
Agents with a rules directory. Each needs its own frontmatter to mark the rule as always-on, so the block writes that frontmatter and then appends the rule body.
omp:
mkdir -p ~/.omp/agent/rules
{ printf -- '---\nalwaysApply: true\n---\n\n'
curl -fsSL https://raw.githubusercontent.com/ctxr-dev/no-comments/main/rules/no-comments.md
} > ~/.omp/agent/rules/no-comments.mdalwaysApply: true is not optional here. omp discovers a rule file that has no alwaysApply, no description and no trigger condition, then drops it — the file would sit on disk doing nothing. For one project only, write to .omp/rules/no-comments.md instead.
Cursor (project):
mkdir -p .cursor/rules
{ printf -- '---\ndescription: Never write a comment on a line you add or change\nglobs:\nalwaysApply: true\n---\n\n'
curl -fsSL https://raw.githubusercontent.com/ctxr-dev/no-comments/main/rules/no-comments.md
} > .cursor/rules/no-comments.mdcWindsurf (project):
mkdir -p .windsurf/rules
{ printf -- '---\ntrigger: always_on\n---\n\n'
curl -fsSL https://raw.githubusercontent.com/ctxr-dev/no-comments/main/rules/no-comments.md
} > .windsurf/rules/no-comments.mdAgents that read one Markdown context file. Same block for all of them — set FILE from the table, then run it. It is safe to re-run: the marker pair is deleted and rewritten, so you never get two copies.
FILE=~/.codex/AGENTS.md # pick your path from the table below
mkdir -p "$(dirname "$FILE")" && touch "$FILE"
sed -i.bak '/<!-- BEGIN no-comments -->/,/<!-- END no-comments -->/d' "$FILE" && rm -f "$FILE.bak"
{ echo '<!-- BEGIN no-comments -->'
curl -fsSL https://raw.githubusercontent.com/ctxr-dev/no-comments/main/rules/no-comments.md
echo '<!-- END no-comments -->'
} >> "$FILE"| Agent | FILE |
Scope |
|---|---|---|
| Codex CLI | ~/.codex/AGENTS.md |
user |
| Gemini CLI | ~/.gemini/GEMINI.md |
user |
| OpenCode | ~/.config/opencode/AGENTS.md |
user |
| GitHub Copilot | .github/copilot-instructions.md |
project |
Any other agent that reads AGENTS.md |
AGENTS.md |
project |
Codex inlines the body because it does not expand @path imports. Gemini CLI and omp do expand them, so you can point at a clone instead of copying — @~/src/no-comments/rules/no-comments.md on its own line.
Four authored illustrations of what changes. Nothing here is measured.
A "why" comment becomes a restructure
Without:
// Clear the cache before saving. A concurrent read otherwise repopulates it
// from the old row, and the stale value sticks around.
cache.delete(key);
await repo.save(record);With:
await repo.saveAndInvalidate(record);One call site, one order, nothing left to warn about.
A doc block becomes a signature
Without:
/**
* Sends the email. Retries on failure.
* @param to - the recipient's address
* @param body - the message body
* @param n - how many times to retry
* @returns whether the provider accepted it
*/
export function send(to: string, body: string, n: number): boolean;With:
export function sendWithRetries(
recipient: EmailAddress,
body: EmailBody,
maxAttempts: number,
): Accepted | Rejected;The types say what the prose said, and the compiler keeps them honest.
Test narration becomes a test name
Without:
it("works", () => {
// Arrange
const cart = new Cart([item(10), item(5)]);
// Act
const total = cart.total();
// Assert
expect(total).toBe(15);
});With:
it("sums the price of every line item", () => {
const cart = new Cart([item(10), item(5)]);
expect(cart.total()).toBe(15);
});A marker becomes a tracked line
Without:
// TODO: only handles the EU region, add US before launch
func rate(region string) Rate {
return euRates[region]
}With:
func euRate(region string) Rate {
return euRates[region]
}and one line in the PR description: only EU rates are wired up; US rates are DEV-4102, which blocks launch. The name says what it covers, and the work is somewhere a human will actually see it.
| Left exactly as it is | Why |
|---|---|
| Linter and compiler pragmas | eslint-disable, @ts-expect-error, # noqa, //nolint. A tool reads them, and deleting one changes the build |
| Shebangs | #!/usr/bin/env bash is read by the kernel, not by a person |
| Mandated license headers | A legal policy put them there |
| Codegen markers | DO NOT EDIT. and @generated make tooling skip the file |
| Framework-parsed annotations | Deleting one deletes a route or a schema |
| Comments that were already in the file | Not yours to clean up. It inflates the diff and buries the real change |
| Anything outside the source | PR text, commit messages, ADRs, docs, the wiki. That is where prose belongs |
The one exception: a comment your change made wrong gets deleted, not rewritten.
Comments rot silently while the code moves on, and a wrong comment is worse than none because it is trusted. A comment explaining a name is a naming bug with a workaround attached. Comment churn inflates diffs and hides the real change from reviewers. And the prose that genuinely matters — why this trade-off, what was rejected, which upstream bug this dodges — belongs in the PR, the commit message, or an ADR, where it is dated, reviewed, searchable, and read by the people who need it.
SKILL.md the skill your agent reads, 152 lines
rules/no-comments.md the always-on rule, 63 lines
README.md this file
references/readable-code.md how to make code that needs no comment, 660 lines
references/pragmas.md the per-language list of directives that are not comments, 261 lines
LICENSE MIT
SKILL.md sits at the repo root, so the skills CLI resolves it with no flags. It carries the procedure: the three hard limits, the six-rung ladder, and the situations that come up most. rules/no-comments.md is deliberately short because it loads on every turn, and it carries instructions only — the technique and the reasoning live in the references and in this file, where they cost nothing at runtime.
references/readable-code.md is the part that does the real work. Banning comments is easy; writing code that does not need one is the skill, so this file catalogues the moves — naming, extraction, types that make an illegal state unrepresentable, guard clauses, dispatch tables, tests that carry the claim, failure types, module shape — each keyed to the comment it removes. It opens with a routing table: the comment was going to say X, so the move is Y. Nothing in it is language-specific. The examples use a neutral notation rather than any real syntax, so the technique is what transfers, not the idiom.
references/pragmas.md holds the per-language list of tokens that look like comments and are not. It matters more than it sounds: without it, an agent tidying up comments will happily delete an eslint-disable, a # type: ignore, or a //go:build line, and each of those changes behaviour.
MIT. See LICENSE.