Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/claude-plugin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@systemfsoftware/claude-code-comment-checker': minor
---

This repository is also a Claude Code plugin. Enabling it runs a PostToolUse hook that tries `comment-checker --strip`, then `direnv exec`. If both miss and the project has `flake.nix`, the error tells you to run `direnv allow` or `nix develop`.
13 changes: 13 additions & 0 deletions .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"name": "comment-checker",
"version": "0.2.0",
"description": "PostToolUse hook that flags unnecessary comments. Runs comment-checker --strip, then direnv exec if it is missing. Names flake.nix when that is why it is missing.",
"author": {
"name": "systemfsoftware",
"url": "https://github.com/systemfsoftware/comment-checker"
},
"homepage": "https://github.com/systemfsoftware/comment-checker",
"repository": "https://github.com/systemfsoftware/comment-checker",
"license": "Apache-2.0",
"keywords": ["hooks", "comments", "claude-code"]
}
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,12 @@ Add the hook to user (`~/.claude/settings.json`) or project (`.claude/settings.j
}
```

Or install this repo as a Claude Code plugin. The hook runs `comment-checker --strip`, then `direnv exec` if that binary is missing. A `flake.nix` in the project makes the error tell you to `direnv allow` or `nix develop` (the flake wraps the checker in bwrap). Deno must be on PATH.

```bash
claude --plugin-dir .
```

On `Edit` and `MultiEdit`, only the comments *added* by the edit are checked — pre-existing comments are left alone. Edits also arrive as fragments, so restatement detection is disabled on them to avoid false positives.

### Verify the wiring
Expand Down
9 changes: 9 additions & 0 deletions hooks/deno.jsonc
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"lock": "./deno.lock",
"imports": {
"@std/fs": "jsr:@std/fs@1.0.19",
"@std/io": "jsr:@std/io@0.225.2",
"@std/path": "jsr:@std/path@1.1.6",
"arktype": "npm:arktype@2.2.3"
}
}
60 changes: 60 additions & 0 deletions hooks/deno.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 14 additions & 0 deletions hooks/hooks.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"PostToolUse": [
{
"matcher": "Write|Edit|MultiEdit",
"hooks": [
{
"type": "command",
"command": "\"${CLAUDE_PLUGIN_ROOT}/hooks/run.ts\"",
"timeout": 30
}
]
}
]
}
61 changes: 61 additions & 0 deletions hooks/run.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
#!/usr/bin/env -S deno run --allow-read --allow-run=comment-checker,direnv --allow-env=CLAUDE_PROJECT_DIR,PATH,HOME

import { exists } from '@std/fs/exists'
import { writeAll } from '@std/io/write-all'
import { join } from '@std/path'
import { type } from 'arktype'

const Env = type({
CLAUDE_PROJECT_DIR: type('string.trim').pipe(type('string').atLeastLength(1)),
})

const env = Env({
CLAUDE_PROJECT_DIR: Deno.env.get('CLAUDE_PROJECT_DIR') ?? '',
})

if (env instanceof type.errors) {
await writeAll(
Deno.stderr,
new TextEncoder().encode(`CLAUDE_PROJECT_DIR must be set by the hook host\n${env.summary}\n`),
)
Deno.exit(1)
}

async function run(cmd: string, args: string[]): Promise<number | undefined> {
try {
const { code } = await new Deno.Command(cmd, {
args,
stdin: 'inherit',
stdout: 'inherit',
stderr: 'inherit',
}).output()
return code
} catch (error) {
if (error instanceof Deno.errors.NotFound) return undefined
throw error
}
}

const strip = ['--strip']
const projectDir = env.CLAUDE_PROJECT_DIR

const fromPath = await run('comment-checker', strip)
if (fromPath !== undefined) Deno.exit(fromPath)

const fromDirenv = await run('direnv', ['exec', projectDir, 'comment-checker', ...strip])
if (fromDirenv !== undefined) Deno.exit(fromDirenv)

const flake = await exists(join(projectDir, 'flake.nix'))
await writeAll(
Deno.stderr,
new TextEncoder().encode(
[
'comment-checker did not run, so nothing checked this write.',
flake
? 'This project has flake.nix. Run direnv allow or nix develop so comment-checker is on PATH (the flake wraps it in bwrap).'
: 'Install it: pnpm add -g @systemfsoftware/claude-code-comment-checker',
'',
].join('\n'),
),
)
Deno.exit(1)