{"slug": "linting-but-this-time-it-s-personal", "title": "Linting, but this time it's personal", "summary": "A developer fed up with AI coding agents such as Claude Code filling source files with redundant inline comments built a personal linting setup that runs a linter dispatch script after every Edit or Write tool call, using Claude Code's PostToolUse hook with an Edit|Write matcher and a 120-second timeout. The author adopted a strict no-inline-comment policy across PHP, TypeScript and Go, and reports that a Go linter built on the go/analysis package runs in 0.12s using 16 MB once Go's build cache is warm, measured on a roughly 500-line file full of linting errors run five times per tool.", "body_md": "[← All writing](https://dblc.fyi/blog/)\n\n# Linting, but this time it's personal\n\nOne thing that has *really* **really** annoyed me using Agentic coding tools is the models littering comments everywhere. They spread like the plague, one missed comment and before you know it there’s code in your commentbase.\n\nI’ve had mixed experiences with trying to add some variation of “No comments” to `AGENTS.md`. They still leave them. Normally I’d catch them when I review my own PRs. But that’s wasting my own energy on a task that isn’t reviewing the changes.\n\nThis is where linting got *personal* for me. I started experimenting with hooks on a vendetta to remove useless comments. It has since grown into a more personal configuration to try and optimise my changes for readability.\n\n## Micro-history\n\nThe original [Lint](<https://en.wikipedia.org/wiki/Lint_(software)>) tool was developed in *1978*, to perform static analysis on C source code. The idea behind it was to allow the developer to concentrate\non the development of the program.\n\n## Modern Era\n\nModern linters can enforce coding conventions across a repository, catch potential bugs and automatically fix certain issues. They are often blazingly fast too. 🦀🔥\n\nThe typical flow may go something like:\n\nIf you are anything like me you’d forget to run the linter during development. I would create my pull request only to find it cannot be merged because it errors in CI/CD.\n\n## Hooks\n\nHooks are lifecycle events that either happen per session or per turn. I am using [Claude Code hooks](https://code.claude.com/docs/en/hooks) (other brands of hooks are available). The main events we are going to focus on here are:\n\n- PostToolUse used to run a hook after a tool call succeeds.\n- PreToolUse used to run before a tool call executes and can block them from executing.\n\nThe [tools](https://code.claude.com/docs/en/tools-reference#tools-reference) we are interested in here are the ones Claude uses to write with:\n\n- Bash executes shell commands.\n- Edit makes targeted edits to specific files.\n- Write creates a new file or overwrites existing ones.\n\nWhen Claude has finished using the `Edit` or `Write` tool, Claude Code fires the `PostToolUse` event. Our hook configuration has a PostToolUse entry with the matcher Edit|Write.\n\nThis can trigger a shell command, in this case it runs a linter dispatch script with the argument `post-edit`.\n\n## settings.json\n\n```\n{\n  \"hooks\": {\n    \"PostToolUse\": [\n      {\n        \"matcher\": \"Edit|Write\",\n        \"hooks\": [\n          {\n            \"type\": \"command\",\n            \"command\": \"node \\\"$HOME/.claude/lint/dispatch.mjs\\\" post-edit\",\n            \"timeout\": 120\n          }\n        ]\n      }\n    ]\n  }\n}\n```\n\n## My first lint rule\n\nI will use Go to demonstrate what I was trying to avoid:\n\n## example.go\n\n```\npackage main\n\nimport \"fmt\"\n\nfunc main() {\n\t// print a greeting\n\tprinter(\"Hello World\")\n}\n\n// Takes a string parameter and prints it using fmt.Println\nfunc printer(s string) {\n\tfmt.Println(s)\n}\n```\n\nAfter reading enough LLM written code, I decided on a strict no inline comment policy. Out of all the inline comments I’ve seen models write almost all of them are redundant. They usually follow the same format, either narrating the code or explaining why something wasn’t done a certain way.\n\nIn my experience the Doc comments (DocBlock in PHP or TSDoc in TS) are usually more *reasonable*. But they also can grow into a mega comment depending on the function.\n\n## Personal Linting\n\nThe primary languages I use day to day are: PHP, TypeScript and sometimes Go. I needed to create a solution that would work in each of the languages. These tools will only be run on files that are modified. They needed to run after every write, so they had to be fast.\n\nAll the numbers that follow are from a micro benchmark: one file of roughly 500 lines, full of linting errors, run five times per tool.\n\nGo was the easiest one. I could just use the go/analysis package. Once Go’s build cache is warm, it takes 0.12s and 16 MB.\n\nFor PHP, I tried [Mago](https://mago.carthage.software/main/en/), a Rust-based static analyser and linter, vs [PHPStan](https://phpstan.org/), a PHP based static analyser. Both of these tools supported custom rules, which I needed. Unsurprisingly, Mago was much quicker: 0.10s vs 3.1s for PHPStan, or 1.5s with its result cache. On one file, Mago also used less memory: 104 MB vs 218 MB, or 108 MB for PHPStan with its cache.\n\nThat changes on an actual codebase. Mago reads everything into memory first, even for one file, so on our largest project it needs about 1.7 GB. PHPStan only loads each class it needs, so its memory grows gradually.\n\nFor TypeScript, I wanted to use [Biome](https://biomejs.dev/), but it didn’t support matching comments, which was my original use case.\n\nI decided to do something experimental: I tried the official TypeScript 7.1 Beta compiler, written in Go, and [tsrs](https://github.com/oxc-project/tsrs), a Rust port of the TypeScript 7.1 checker.\n\nI chose tsrs because it was the faster of the two and the most memory efficient. It type-checked the file in 0.15s with 37 MB, compared to 0.33s and 70 MB for the 7.1 Beta.\n\n## Dispatch script\n\nWhen a file is edited in a project, be it PHP, TS or Go. The `dispatch.mjs` script first checks the file extension to determine the language. Then it looks for `go.mod`, `composer.json` or `tsconfig.json` to find the root of the project.\n\nThis then runs the linting tool that I have chosen for the language on modified files.\n\nIn order for this to work correctly it needs to establish a baseline. This is a lint of the entire project in its pre-edited state. This is done on a per session basis. Any existing violations of my personal rules are recorded as part of the baseline because we only care about new changes.\n\nGit is used to identify when a change happened outside of Claude’s session so like a merge or a pull.\n\nWhen Claude calls the Edit or Write tool, Claude Code uses the PreToolUse Hook to run our dispatch script to capture the before state of the files.\n\n## Lint roller in action\n\nWhenever Claude litters my changes with comments, it receives a fine: \n\n```\n{\n  \"decision\": \"block\",\n  \"reason\": [\n    \"Personal lint found 1 error(s) in your change to example.go. Fix them now:\",\n    \"- example.go:6, [no-inline-comment] Inline comment: // print a greeting\"\n  ]\n}\n```\n\n## hook-output.json\n\nThis gives Claude the information it needs to remove the problematic line instantly and now, I never have to write another comment on a PR saying “remove this comment you *£%^&*”.\n\nI’ve since expanded my personal rules to optimise the changes for readability and simplicity. Using a vibes-based combination of cyclomatic complexity, halstead complexity and cognitive complexity.", "url": "https://wpnews.pro/news/linting-but-this-time-it-s-personal", "canonical_source": "https://dblc.fyi/blog/linting-but-this-time-it-is-personal/", "published_at": "2026-11-15 17:33:23+00:00", "updated_at": "2026-11-15 17:54:18.946162+00:00", "lang": "en", "topics": ["ai-agents", "ai-tools", "developer-tools", "ai-products"], "entities": ["Claude Code", "Anthropic", "Go", "PHP", "TypeScript", "go/analysis"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/linting-but-this-time-it-s-personal", "markdown": "https://wpnews.pro/news/linting-but-this-time-it-s-personal.md", "text": "https://wpnews.pro/news/linting-but-this-time-it-s-personal.txt", "jsonld": "https://wpnews.pro/news/linting-but-this-time-it-s-personal.jsonld"}}