diff --git a/.changeset/claude-plugin.md b/.changeset/claude-plugin.md new file mode 100644 index 0000000..0c8702e --- /dev/null +++ b/.changeset/claude-plugin.md @@ -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`. diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json new file mode 100644 index 0000000..9e31e2b --- /dev/null +++ b/.claude-plugin/plugin.json @@ -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"] +} diff --git a/README.md b/README.md index 794a124..83b4ba2 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/hooks/deno.jsonc b/hooks/deno.jsonc new file mode 100644 index 0000000..a8eaf7c --- /dev/null +++ b/hooks/deno.jsonc @@ -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" + } +} diff --git a/hooks/deno.lock b/hooks/deno.lock new file mode 100644 index 0000000..e08dbf5 --- /dev/null +++ b/hooks/deno.lock @@ -0,0 +1,60 @@ +{ + "version": "5", + "specifiers": { + "jsr:@std/fs@1.0.19": "1.0.19", + "jsr:@std/internal@^1.0.14": "1.0.14", + "jsr:@std/io@0.225.2": "0.225.2", + "jsr:@std/path@1.1.6": "1.1.6", + "npm:arktype@2.2.3": "2.2.3" + }, + "jsr": { + "@std/fs@1.0.19": { + "integrity": "051968c2b1eae4d2ea9f79a08a3845740ef6af10356aff43d3e2ef11ed09fb06" + }, + "@std/internal@1.0.14": { + "integrity": "291516b3d4c35024d6ffbc0a9df5bf4c64116e05b50012cf846710152d2ffdf7" + }, + "@std/io@0.225.2": { + "integrity": "3c740cd4ee4c082e6cfc86458f47e2ab7cb353dc6234d5e9b1f91a2de5f4d6c7" + }, + "@std/path@1.1.6": { + "integrity": "c68485c2a4dfbb5ae3cc74fae4e8c4e5d874cf8a8ed12927917235c758b46cbe", + "dependencies": [ + "jsr:@std/internal" + ] + } + }, + "npm": { + "@ark/schema@0.56.2": { + "integrity": "sha512-Qx4D2JFbBWpntiHZaTv7bGG4H/M2rigiknezKg/WVyDSaLdE4YCcWAOoFB7pjjDqHbbV2OqRfntm1nnXvwMexg==", + "dependencies": [ + "@ark/util" + ] + }, + "@ark/util@0.56.2": { + "integrity": "sha512-9kU2sUE38FZEGG7l3hamYMBieLYEJh2L1mrYD2eXpT+78EnQSV1bhjxJhnxGBMSTbtwpBSDNSK+K60WvaI/DTQ==" + }, + "arkregex@0.0.8": { + "integrity": "sha512-PJcx6G1kQTgLKPUbeYlYecDRaKq15AMSGVajlKFYWlPeJRQL+j3dKE6tyMs40HZ99djS1l9Vhl3ezAHy9JBIqQ==", + "dependencies": [ + "@ark/util" + ] + }, + "arktype@2.2.3": { + "integrity": "sha512-7W+0RLTUNJiBFIIZXwOQxSR8Z273IAd6IvqBeG9+gHnQKFsIx2C0iOtGTmMrPnlX4qLXyc5+ll7A0BIj9WrbTg==", + "dependencies": [ + "@ark/schema", + "@ark/util", + "arkregex" + ] + } + }, + "workspace": { + "dependencies": [ + "jsr:@std/fs@1.0.19", + "jsr:@std/io@0.225.2", + "jsr:@std/path@1.1.6", + "npm:arktype@2.2.3" + ] + } +} diff --git a/hooks/hooks.json b/hooks/hooks.json new file mode 100644 index 0000000..8fb86be --- /dev/null +++ b/hooks/hooks.json @@ -0,0 +1,14 @@ +{ + "PostToolUse": [ + { + "matcher": "Write|Edit|MultiEdit", + "hooks": [ + { + "type": "command", + "command": "\"${CLAUDE_PLUGIN_ROOT}/hooks/run.ts\"", + "timeout": 30 + } + ] + } + ] +} diff --git a/hooks/run.ts b/hooks/run.ts new file mode 100755 index 0000000..58f40c3 --- /dev/null +++ b/hooks/run.ts @@ -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 { + 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)