evilmartians

Stars and mentions

Last 30 days, Oct 4 – Nov 2

GitHub stars0
mentions6
01234Oct 4Oct 18Nov 2
UTC days · repositories evilmartians owns0 repositories

Package downloads

No package download history available yet.

Top repositories

evilmartians’s repositories ranked by new stars, as on trending.

Loading

Contributes to

Repositories outside evilmartians’s account where evilmartians opened or reviewed pull requests in the past 365 days.

Who noticed evilmartians

Everyone who starred or forked evilmartians’s repositories since the archive began in 2011.

Notable accounts

Ordered by the stars their own repositories have received.

Biggest fans

Accounts that starred the most of these repositories.

Breakout weeks

Each repository’s best week of new stars, with the release published around it.

Mentions

6posts
Nov 30 posts
  1. Beyond Prompts: Enforcing Repository Rules With Git Hooks You have probably already tried the obvious thing. You wrote the rules down - in AGENTS.md, CLAUDE.md, .cursor/rules, or whatever file your agent reads first. Do not commit without tests. Do not leave a component without a story. Run the type checker before you say you are done. The agent agreed, enthusiastically, and then went ahead and did something else anyway. This is not a prompt-quality problem, and it is not specific to any one model. Large language models optimize for the next plausible token, not for your repository's conventions. In a small codebase you can review every diff and catch the drift by hand. In a large one you cannot: the agent touches a dozen files across a monorepo, the formatting is off, a component is missing its story, a test that used to pass now does not, and the commit lands anyway because nobody is running the checks at the moment the change is written. The fix is not a better prompt; it is a gate. A Git hook runs your checks on every change, at git commit and git push, whoever - or whatever - wrote it. The tool I use to manage them is Lefthook: a single, dependency-free Go binary, driven by one YAML file, able to run jobs in parallel and re-stage the files it fixes. It reaches past Git, too: an ai: block (currently beta) lets Claude Code, Cursor, Codex, and Copilot run Lefthook hooks on their own events, so your checks fire when the agent stops working - before anything is ever committed. What follows is a working setup: the Git hooks first, then the same checks wired into the agent. Quick Reference If you want the short answer before the walkthrough, this is the whole tool at a glance: | Category|Details | | --- | --- | | What it is | A hooks manager for Git and for coding agents; one binary, no runtime dependency | | Config file | lefthook.yml, or a .lefthook.yaml / .toml / .json variant | | Install the hooks | lefthook install | | Run a hook by hand | lefthook run pre-commit | | Validate the config | lefthook validate | | Skip once | LEFTHOOK=0 git commit | | Agent hooks (beta) | Run your Lefthook hooks on Claude Code, Cursor, Codex, or Copilot events | | Current version | v2.1.17 (October 2026) | The config we build in the next sections wires up five checks on commit and two on push; later, the same checks are attached to your coding agent's events. Why Instructions Are Not Enforcement A written rule is advisory. The agent may follow it, may partially follow it, or may follow it for the first three files and forget by the fourth - and you will not know which until you read the diff. A hook is deterministic. It runs the same command on the same files every single time, it fails closed, and it does not care who authored the change. If the check fails, the commit does not happen. That difference matters more, not less, as codebases grow. Instructions have to be read, remembered, and applied; enforcement is automatic. It also gives you a single place to put the rules that are specific to your project - the ones no linter ships with, like "every component must have a story" - instead of repeating them across agent instruction files and hoping. Lefthook fits here because it is small. It is a Go binary with no runtime to install, and it reads lefthook.yml on every hook run, so you never reinstall after editing the config - for Git hooks or for the agent hooks we add later. Install Lefthook The most common route is the package manager you already use. For a Node project that is the lefthook npm package: pnpm add -D lefthook # or: npm install --save-dev lefthook / yarn add --dev lefthook There are equivalents for almost every ecosystem, all documented in the installation guide: brew install lefthook on macOS and Linux, go install github.com/evilmartians/lefthook/v2@v2.1.17, pipx install lefthook, gem install lefthook, plus apt, rpm, apk, winget, scoop, snap, and mise. If you would rather not use a package manager at all, download the binary from the latest release and put it on your PATH; lefthook self-update keeps it current. For CI the mechanics are slightly different, because Git hooks are a local, client-side feature. Nothing runs your pre-commit hook in a container that just cloned the repo - there is no .git/hooks/pre-commit doing work unless you install it. So the CI job installs Lefthook and then calls the hook explicitly: pnpm add -D lefthook pnpm exec lefthook run pre-commit --all-files --all-files replaces {staged_files} with every tracked file, which is what you want when there is no index to speak of. There is no official Lefthook GitHub Action and no official Docker image, so in CI you install the binary yourself - brew, curl, npm, or your base image's package manager - and call the hook explicitly. One detail to know: when the npm package is installed with CI=true set, its postinstall script deliberately skips hook installation, so a CI run will not try to wire up .git/hooks that nothing will execute. Your CI step calling lefthook run is what does the work. Step 1: Write the Config The whole configuration is one file at the repository root. This is the shape I use on a TypeScript/React project, and it is the config the runs below use: # lefthook.yml min_version: 2.1.10 lefthook: pnpm exec lefthook glob_matcher: doublestar pre-commit: jobs: - name: format staged files glob: "**/*.{js,jsx,ts,tsx,json,md,yml,yaml,css}" run: pnpm format {staged_files} stage_fixed: true - name: lint staged files glob: "**/*.{js,jsx,ts,tsx}" run: pnpm lint {staged_files} stage_fixed: true - name: typecheck run: pnpm check:types - name: stories-present glob: "src/components/**/*.{ts,tsx}" exclude: "**/*.stories.{ts,tsx}" run: pnpm check:stories {staged_files} - name: unit tests run: pnpm test:unit pre-push: parallel: true jobs: - name: full verification run: pnpm verify - name: storybook tests run: pnpm test:storybook Seven jobs in total, split across the two hooks: | Job | Hook | What it enforces | | --- | --- | --- | | format staged files | pre-commit | Prettier rewrites staged files, then Lefthook re-stages them | | lint staged files | pre-commit | ESLint --fix rewrites staged files, then re-stages them | | typecheck | pre-commit | tsc --noEmit over the whole project | | stories-present | pre-commit | A custom rule: every component has a story file | | unit tests | pre-commit | node --test | | full verification | pre-push | Typecheck, tests, and the custom checks together | | storybook tests | pre-push | Component-level tests | A few keys carry most of the weight. The format, lint, check:*, and test:* targets are your own package.json scripts, so the config stays tool-agnostic: replacing Prettier with Biome is a change to one script body, not to seven hook entries. jobs is the modern list form (the older commands: map still works, but jobs also support scripts and groups). glob filters the file list a job receives, and exclude removes files from that filtered list. run is the command, executed with sh. The {staged_files} template expands to the files staged for the current commit - for a pre-push job you would use {push_files}, and {all_files} gives you everything tracked. stage_fixed: true is the one that changes how it feels to use. When a job rewrites its files - Prettier reformatting, ESLint applying an autofix - Lefthook runs git add on them afterward, so the fixes land in the same commit instead of leaving a dirty working tree behind. It only works on pre-commit, and because each of those jobs mutates the index, I keep the auto-fixing jobs sequential by leaving parallel at its default (false). If the git add fails, Lefthook fails the hook rather than let the commit go through with unfixed content. If you would rather fail than let a hook rewrite files, invert the two auto-fixing jobs: run prettier --check and eslint without --fix, drop stage_fixed, and the pre-commit hook becomes a pure, parallel-safe gate. The typecheck, stories-present, and unit tests jobs do not use {staged_files}; they run against the whole project, because a change in one file can break a type or a test somewhere else. The stories-present job is the interesting one for the agent narrative: it is a rule that exists only in this repository. Its glob is the part that matters, and it is also the part that is easy to get wrong - see [The Glob That Silently Disables Your Hook](#the-glob-that-silently-disables-your-hook). The pre-push hook is deliberately broader and parallel. It runs pnpm verify (types, unit tests, and the custom story check) alongside the component tests. This is the layer that catches what a distracted --no-verify commit slipped past, and it is the right place for the slower checks you do not want on every keystroke. Step 2: Install and Run the Hooks Install the hooks once per clone: pnpm exec lefthook install sync hooks: ✔️(pre-push, pre-commit) That command writes small shell scripts into .git/hooks/ for each configured hook; each script simply calls lefthook run <hook>. Because the config is read fresh every time a hook runs, editing lefthook.yml never requires reinstalling. You can confirm the hooks are present and synchronized with: pnpm exec lefthook validate # config is well-formed pnpm exec lefthook check-install # exit 0 if hooks are installed and in sync To run a hook without committing, lefthook run is the entry point. lefthook run pre-commit runs the hook as-is, lefthook run pre-commit --all-files forces every tracked file through it, --file path/to/file forces a specific list, and --job or --tag narrows it to specific jobs. Any hook name is a valid target, so a custom block such as verify: becomes lefthook run verify. Two escape hatches are worth knowing. LEFTHOOK=0 git commit disables hooks for a single command, and git commit --no-verify bypasses hooks at the Git level. I mention them because they are necessary - but they are also exactly why the push hook and CI matter: a rule enforced only at commit time is a rule that a tired developer or a confident agent can skip. Finally, teams rarely want identical hooks on every machine. A lefthook-local.yml file (git-ignored) merges over lefthook.yml, so an individual can set skip: true on a slow job or re-tag a command without touching the shared config. What It Looks Like When a Commit Is Blocked Here is a real run, on a small demo repository, of an agent finishing a change and committing it. The change adds a greet helper written in double quotes with a let that is never reassigned, and a Modal component with no story file. The first attempt: $ git commit -m "feat: add greeting helper and modal component" ╭───────────────────────────────────────╮ │ lefthook v2.1.17 hook: pre-commit │ ╰───────────────────────────────────────╯ ┃ format staged files ❯ $ prettier --write lefthook.yml src/components/Modal.ts src/greeting.ts lefthook.yml 12ms src/components/Modal.ts 18ms (unchanged) src/greeting.ts 3ms ┃ lint staged files ❯ $ eslint --fix src/components/Modal.ts src/greeting.ts ┃ typecheck ❯ $ tsc --noEmit ┃ stories-present ❯ $ node scripts/check-stories.mjs src/components/Modal.ts Missing story files for: src/components/Modal.ts [ELIFECYCLE] Command failed with exit code 1. exit status 1 ... ──────────────────────────────────── summary: (done in 1.24 seconds) ✓ format staged files (0.26 seconds) ✓ lint staged files (0.44 seconds) ✓ typecheck (0.37 seconds) ✓ unit tests (0.11 seconds) ✗ stories-present (0.05 seconds) The commit does not happen. Two different things occurred in that run, and they are the two halves of why this works. First, format and lint silently fixed the sloppy code: Prettier rewrote the quotes and style, ESLint turned the let into a const, and stage_fixed re-staged both. You can prove the fixed content is what is staged, not just present in the working tree: $ git show :src/greeting.ts export function greet(name: string) { const message = 'Hello, ' + name + '!' return message } Second, stories-present refused the change. The component exists, the story does not, and no amount of instruction-following on the agent's part makes the commit land. Add the missing story and the same commit passes, every job green: $ git add src/components/Modal.stories.ts $ git commit -m "feat: add greeting helper and modal component" ... ──────────────────────────────────── summary: (done in 1.17 seconds) ✓ format staged files (0.14 seconds) ✓ lint staged files (0.47 seconds) ✓ typecheck (0.38 seconds) ✓ stories-present (0.05 seconds) ✓ unit tests (0.12 seconds) [main de227fe] feat: add greeting helper and modal component 4 files changed, 17 insertions(+), 4 deletions(-) git push runs the second gate. Both jobs run in parallel: $ git push origin main ╭─────────────────────────────────────╮ │ lefthook v2.1.17 hook: pre-push │ ╰─────────────────────────────────────╯ ┃ storybook tests ❯ $ node scripts/check-stories.mjs Story check passed for 2 component file(s). ┃ full verification ❯ $ pnpm check:types && pnpm test:unit && pnpm check:stories $ tsc --noEmit $ node --test "src/**/*.test.ts" ✔ adds two numbers (0.467667ms) ℹ pass 1 ℹ fail 0 $ node scripts/check-stories.mjs Story check passed for 2 component file(s). ──────────────────────────────────── summary: (done in 0.65 seconds) ✓ storybook tests (0.06 seconds) ✓ full verification (0.55 seconds) The Glob That Silently Disables Your Hook The config above includes glob_matcher: doublestar, and that one line is doing more than it looks. A pattern like src/components/**/*.{ts,tsx} reads as correct, but without doublestar it never matches a file sitting directly in src/components/. Here is what that looks like on a commit that adds src/components/Modal.ts with no story: $ git commit -m "feat: add modal component" ... │ stories-present (skip) no files for inspection ──────────────────────────────────── summary: (done in 1.10 seconds) ✓ format staged files (0.11 seconds) ✓ lint staged files (0.49 seconds) ✓ typecheck (0.38 seconds) ✓ unit tests (0.12 seconds) [main 511f599] feat: add modal component 1 file changed, 3 insertions(+) The check did not fail. It did not run at all. In Lefthook's default matcher (gobwas), ** matches one or more directories, not zero or more, so src/components/**/*.ts requires at least one directory between src/components/ and the file - it matches src/components/Button/Button.ts but not src/components/Modal.ts. With no matching files, Lefthook prints (skip) no files for inspection and moves on, and the commit succeeds. A guard you believe is watching is worse than no guard, because you stop checking by hand. There are two ways out. The recommended one is to switch to standard glob semantics for the whole config: glob_matcher: doublestar With doublestar, **/ matches zero or more directories, so src/components/**/*.{ts,tsx} matches Modal.ts as expected - this is the reason the setting appears at the top of the config above. The catch is that the same setting also makes * separator-aware: a bare *.ts now matches only files in the repository root, where the default matcher had let * cross /. That is why every file glob in the config above uses **/*.{...}: with doublestar, that is how you say "at any depth". The second option is to keep the default matcher and write the pattern for its rules: because * crosses / there, src/components/*.{ts,tsx} already matches at any depth. That works, but it depends on a matcher behavior most people do not expect, so I prefer the explicit switch. Either way, the lesson generalizes: after you add a file-filtering rule, verify it actually fires. Lefthook's (skip) no files for inspection line in the output is the tell. Make the Agent Run the Checks Too The hooks above fire on git commit and git push. An agent that edits twenty files and stops - without committing - never triggers them. Lefthook has a beta feature for exactly this: an ai: block that declares which of your hooks an agent should call on which of its own events. During lefthook install, Lefthook generates the provider's settings file for you. ai: claude: Stop: verify cursor: stop: verify verify: jobs: - name: typecheck run: pnpm check:types - name: unit tests run: pnpm test:unit Stop is Claude Code's event for "the agent finished responding", and stop is the Cursor equivalent; both map to the verify hook that Lefthook then runs. Supported providers are claude, codex, cursor, and copilot. Running lefthook install with the config above updates .claude/settings.json: { "hooks": { "Stop": [ { "hooks": [{ "type": "command", "command": "pnpm exec lefthook run verify" }] } ] } } Note the command. Generated hook commands use the top-level lefthook: value when it is set - here pnpm exec lefthook - and otherwise fall back to the absolute path of the binary that ran install. Since that absolute path is machine-specific and would poison a committed settings file, setting lefthook: to a portable command is the difference between a file your teammates can share and a file that only works on your laptop. This feature is marked beta, so treat the generated output as version-sensitive and keep an eye on it when you upgrade. Lefthook vs. pre-commit vs. Husky | | Lefthook | pre-commit | Husky | | --- | --- | --- | --- | | Runtime | Single Go binary, no runtime | Python framework | Node.js | | Config | One YAML/TOML/JSON file | .pre-commit-config.yaml | Shell scripts in .husky/ | | Runs against | Staged/pushed/all files via templates | Staged files by default | Whatever the script does | | Parallel jobs | Yes | Limited | No (manual) | | Isolated tool envs | No (uses your project's tools) | Yes (per-hook language envs) | No | | Language ecosystems | Any (it just runs commands) | Many, via hook repos | Any, in Node projects | There is no objective winner here; pick the one that fits how your repository already works. If you rely on pre-commit's managed, per-language hook environments, stay there - that isolation is real value. If your repo is Node-based, Husky is familiar and perfectly capable. I reach for Lefthook for three reasons: it is one binary so nothing new has to be installed at runtime, its jobs run in parallel so the hook stays fast, and {staged_files}, stage_fixed, and glob filtering are built into the config rather than pushed into scripts. As always with tools, the right answer is the one your team will actually keep running. Troubleshooting & Gotchas A glob that matches nothing silently skips the job. This is the failure from the section above, and it is the most dangerous one because the hook reports success. ** in the default gobwas matcher means one or more directories; (skip) no files for inspection in the output means your filter is excluding everything. Set glob_matcher: doublestar and use **/ where you mean any depth. stage_fixed only works on pre-commit, and it needs the index. It calls git add after the job rewrites files, so it cannot fix anything on pre-push or a custom hook. Keep auto-fixing jobs on pre-commit and sequential: if several jobs run in parallel and rewrite the index at the same time, keep them out of that batch. --no-verify and LEFTHOOK=0 bypass everything. Hooks are a convenience gate, not a security boundary. For rules that must hold, run the same checks in CI (lefthook run pre-commit --all-files) and make that job required. Do not rely on the client-side hook as your only enforcement. pnpm gates the postinstall script. The lefthook npm package installs hooks from a postinstall script, but pnpm blocks dependency build scripts by default. In pnpm 11/12 the setting moved from pnpm.onlyBuiltDependencies in package.json to an allowBuilds map in pnpm-workspace.yaml; approve it and the postinstall runs. Failing that, lefthook install explicitly does the same thing. Fresh releases can be held back by supply-chain policies. pnpm refused to install the just-published lefthook@2.1.17 because it was inside the default minimumReleaseAge window; minimumReleaseAge: 0 (or waiting out the window) is the adjustment. Useful to know when a version you just saw released will not install yet. A long file list is split into several commands. Command-line length is capped on every OS, so Lefthook runs your command in batches when {staged_files} is large. A linter that prints per-run summaries will print one per batch. Check that the hooks are actually installed. A fresh clone has no hooks until lefthook install runs - the npm postinstall usually handles it, but clones that skip install scripts will not, and CI=true makes the postinstall skip on purpose. lefthook check-install returns exit code 0 only when the hooks are present and in sync, which makes it a cheap guard in a setup script. Conclusion Agentic coding has not changed the need for quality gates; it has made them more valuable, because the volume of change goes up while the attention per change goes down. Writing the rules in an instruction file is still worth doing, but it is not enforcement. A Lefthook config is a few dozen lines of YAML that makes the same checks run on every commit and push, whoever - or whatever - wrote the code, and the beta ai: block extends the same jobs to the agent's own events. Start with one hook: format and lint the staged files, re-stage the fixes, and refuse the commit if the tests fail. In a large codebase, that one gate prevents more bad diffs than any prompt. Resources - Lefthook documentation - Configuration reference - Installation guide - glob_matcher - stage_fixed - ai - agent hooks (beta) - GitHub repository - pre-commit FAQ My AI coding agent keeps committing code that fails lint and tests. How do I stop it? Install Git hooks with Lefthook so the checks run before the commit is created. Add a lefthook.yml with a pre-commit hook whose jobs run your formatter, linter, type checker, and tests, then run lefthook install once per clone. If any Lefthook job exits non-zero, the commit is aborted, so code that fails your checks cannot be committed even if an agent ignored the same rules written in an instruction file. How do I make Git hooks run automatically after every clone without asking each developer to install them? Add Lefthook to your project's dependencies - for example pnpm add -D lefthook or npm install --save-dev lefthook. The package's postinstall script runs lefthook install, which writes the hooks into .git/hooks for each developer automatically. Note that pnpm blocks dependency build scripts by default, so you must approve Lefthook's build in pnpm-workspace.yaml, and a fresh clone can always run lefthook install manually. Why did my Lefthook job not run and print "no files for inspection"? Lefthook's default glob matcher treats ** as one or more directories, so a pattern like src/components/**/*.ts does not match src/components/Modal.ts - it matches nothing, and the job is skipped instead of failing. Set glob_matcher: doublestar in lefthook.yml so **/ matches zero or more directories, and remember that with doublestar a bare *.ts only matches the repository root, so use **/*.ts for files at any depth. How do I make Lefthook commit the files that a formatter fixed? Set stage_fixed: true on the Lefthook job. After the job's command rewrites the staged files, Lefthook runs git add on them, so the corrected content is included in the same commit. stage_fixed works only on the pre-commit hook, and jobs that use it should run sequentially rather than in parallel because they modify the Git index. Can I skip Git hooks for a single commit? Yes. Run LEFTHOOK=0 git commit to disable Lefthook for that one command, or git commit --no-verify to skip Git hooks entirely. Both are intentional escape hatches; because either can bypass a pre-commit hook, rules that must always hold should also be enforced in CI by running lefthook run pre-commit --all-files. How do I run Lefthook hooks in CI when hooks are a local Git feature? Install the Lefthook binary in your CI job, then invoke the hook explicitly instead of relying on .git/hooks: pnpm exec lefthook run pre-commit --all-files. The --all-files flag replaces {staged_files} with every tracked file, since there is no staging area to speak of. Setting CI=true makes the Lefthook npm package skip its postinstall hook installation, which is the desired behavior in CI. Does Lefthook work with Claude Code, Cursor, Codex, and GitHub Copilot? Yes, through the beta ai: block in lefthook.yml. You map a provider's event names - for example Claude Code's Stop or Cursor's stop - to a Lefthook hook, and lefthook install generates the provider's settings file so the agent calls lefthook run <hook> when that event fires. The feature supports claude, codex, cursor, and copilot, and because it is beta its generated files are worth re-checking after a Lefthook upgrade. Is Lefthook faster than pre-commit or Husky? Lefthook is distributed as a single Go binary with no runtime to load, and it can run a hook's jobs in parallel, so it does not pay the startup cost of a Python framework or a Node process. pre-commit's advantage is its isolated per-language hook environments, and Husky's is familiarity in Node projects, so the choice usually comes down to which model fits your repository rather than raw speed.

  2. Your Coding Agent Skips Your Lint. lefthook Makes Every Commit Run It Anyway - Book: Decoupled PHP — Clean and Hexagonal Architecture for Applications That Outlive the Framework - Also by me: Thinking in Go (2-book series) — Complete Guide to Go Programming + Hexagonal Architecture in Go - My project: Hermes IDE | GitHub — an IDE for developers who ship with Claude Code and other AI coding tools - Me: xgabriel.com | GitHub --- git log shows the agent's commit: feat: refund endpoint. Inside it are tabs where your code style wants spaces, a method PHPStan would reject at level 8, and a domain class that now imports an Eloquent model. None of that is the agent being careless. PHP-CS-Fixer, PHPStan and your layer rules exist, but they run when you remember to run them, or in CI after the push. Nothing ran them at the moment the agent typed git commit. So nothing stopped it. A git hook does run at that moment, for the agent the same as for you. This post sets one up with lefthook, on a PHP project, so a bad commit fails in the agent's own terminal and the agent reads the error before you ever see the diff. Then it covers the two ways the gate quietly opens: a fresh worktree, and --no-verify. Step 1: install lefthook as a dev dependency lefthook is a git hooks manager written in Go and shipped as one binary. Its releases page lists v2.2.2 as the latest, from October 13, 2026. For a PHP project, the PHP install docs use Composer: composer require --dev evilmartians/lefthook vendor/bin/lefthook version vendor/bin/lefthook install install writes the hook scripts into git's hooks folder. You run it once per clone, not after every config change: the install docs say lefthook.yml is read every time a hook runs. The hook finds the binary through composer exec, which matters in Step 4. Step 2: a pre-commit that checks staged files only Here is the whole lefthook.yml. Commit it at the repo root. # lefthook.yml assert_lefthook_installed: true glob_matcher: doublestar pre-commit: parallel: true jobs: - name: php-cs-fixer glob: "**/*.php" run: >- vendor/bin/php-cs-fixer fix --path-mode=intersection {staged_files} stage_fixed: true - name: phpstan glob: "**/*.php" run: vendor/bin/phpstan analyse --no-progress {staged_files} - name: deptrac glob: "src/**/*.php" run: vendor/bin/deptrac analyse fail_text: "A layer imports a class it must not." pre-push: jobs: - name: phpstan-full glob: "**/*.php" run: vendor/bin/phpstan analyse --no-progress - name: phpunit glob: "**/*.php" run: vendor/bin/phpunit What each piece does, according to the lefthook docs: {staged_files} expands to the files staged for this commit (run docs). An agent that touched three files gets three files checked, not the whole repo, so the hook stays fast enough that nobody is tempted to switch it off. glob filters that list. When no staged file matches, the job is skipped (glob docs), so a commit that only edits a README runs nothing. The default matcher treats ** as one or more folders, so **/*.php would miss a PHP file at the repo root. glob_matcher: doublestar switches to the usual meaning, where **/*.php matches app.php and src/app.php alike (glob_matcher docs). parallel: true runs the three jobs at the same time (parallel docs). stage_fixed: true makes lefthook call git add on the files after the job runs, and it works only in pre-commit (stage_fixed docs). PHP-CS-Fixer rewrites the formatting, the fixed files go back into the commit, and the agent never has to think about spaces. One catch: git add stages the whole file, so if a file had unstaged edits too, they ride along. --path-mode=intersection tells PHP-CS-Fixer to check only the paths that are both in its config's finder and on the command line (usage docs). Without it, a path you pass overrides your config, and files you meant to exclude get formatted. Deptrac is the layer check. It reads your deptrac.php, where you declare layers such as Domain, Application and Infrastructure and which may depend on which. It analyses the configured paths, not single files, so the job takes no {staged_files}. The glob only decides whether it runs. This is the job that catches the Eloquent import in the domain class, and fail_text adds a line telling the agent what kind of mistake it made (fail_text docs). PHPStan on staged files only has a blind spot: change a method's signature and the caller you didn't touch is not analysed. The pre-push hook closes it with a full PHPStan run plus the test suite. Slower, but it runs once per push instead of once per commit. Run without paths, PHPStan reads them from the paths parameter in your config (CLI docs), so the push job analyses everything phpstan.neon lists. !A coding agent at a desk pushing a commit through a gate that stops it and hands back a note The hook runs inside the agent's own git commit, so the failure lands where the agent can read it. Step 3: what the agent sees when the commit fails Try it by hand first. vendor/bin/lefthook run pre-commit runs the hook without committing (run command), so you can stage a deliberately broken file and watch each job. When an agent such as Claude Code runs git commit, the same hook runs inside that command. A failing job makes the hook exit non-zero, git aborts the commit, and the hook's output is part of what the command returned. The agent gets PHPStan's error with file and line, in the terminal it is already working in, as the result of its own action. That is the difference from CI. A red pipeline after the push needs someone to read it and paste it back. A failed commit is feedback the agent receives inside its loop, so its usual next move is to fix the file and commit again. You can make that explicit with one line in your CLAUDE.md or AGENTS.md: "If a git hook fails, fix the reported problem and commit again. Never bypass the hook." Step 4: the worktree trap, and why the gate opens silently If you run agents in parallel, you give each one its own git worktree. Git keeps the hooks folder in the shared repository directory: the repository layout docs say a linked worktree uses $GIT_COMMON_DIR/hooks. You do not need to run lefthook install again in every worktree. !Two coding agent sessions side by side, Codex and Claude Code, each on its own hermes/ worktree, with the sessions sidebar Hermes 2.3.4: Codex and Claude Code in split panes, each in its own hermes/ worktree, with status, context use, cost and branch in the sidebar. What a new worktree does not have is vendor/. A worktree holds the tracked files only, and vendor/ is ignored. So the hook fires, looks for lefthook through composer exec and the other places on its list and, unless lefthook is also installed globally, finds nothing. Then it prints Can't find lefthook in PATH and lets the commit through. The behaviour comes from lefthook's hook template: unless you opt in to failing, a missing binary is not an error. The agent in that worktree commits unchecked code and never sees a problem. The config above starts with assert_lefthook_installed: true for this reason. With it, the hook exits 1 when the binary can't be found (docs). The commit fails with a message the agent can act on, and the agent can run composer install itself. This option is written into the hook script, since the hook can't read lefthook.yml without the binary, so run vendor/bin/lefthook install once more after you add it. The cleaner fix is to install dependencies before the agent starts. With plain git: git worktree add -b agent/refund ../app-refund cd ../app-refund composer install --no-interaction I build Hermes IDE, so weigh this part with that in mind. Hermes starts each task (⌘N, Ctrl+Shift+N on Windows and Linux) on its own hermes/ branch in its own worktree, and runs a recipe from .hermes/worktree.toml in that worktree before the agent starts (recipe docs): # .hermes/worktree.toml setup = [ "composer install --no-interaction", "vendor/bin/lefthook install", ] done_when = ["vendor/bin/phpunit"] !The Hermes New task sheet with a refund-endpoint task typed and chips for Claude Code, the orders-api project and a new hermes/ worktree Hermes 2.3.4: the ⌘N New task sheet, with Claude Code, the orders-api project, a new hermes/ worktree and the "Hermes will run" preview. Setup commands run in order, and the first one that fails stops the rest, stays on the task's card and goes to the inbox, so a broken composer install is something you see instead of something the agent quietly works around. The lefthook install line is redundant once the main checkout has run it, since the hooks folder is shared, but it costs nothing and covers a repo where nobody ran it yet. Hermes shows you the commands and waits for Run setup the first time it sees the recipe, and again whenever it changes. Skip starts the session without running setup, so leave it alone on repos that depend on the gate. done_when sets the task's default check. Step 5: close --no-verify Every hook has an exit. git commit -n or --no-verify bypasses pre-commit and commit-msg (git-commit), git push --no-verify skips pre-push (git-push), and LEFTHOOK=0 git commit turns lefthook off for one command (LEFTHOOK env). An agent stuck on a failing check can reach for any of them. For Claude Code, add deny rules to .claude/settings.json and commit it: { "permissions": { "deny": [ "Bash(git commit *--no-verify*)", "Bash(git commit -n*)", "Bash(git commit * -n *)", "Bash(git push *--no-verify*)" ] } } In Claude Code's permission rules, * matches any text and can sit anywhere in the pattern, deny beats any allow rule, and a deny rule also applies to each part of a compound command such as cd app && git commit --no-verify. Read the limits on that same page before you trust it. A Bash rule matches the command text Claude writes, and the docs say it is not a security boundary: git -C . push slips past Bash(git push *), and so does sh -c '...'. Short flags can be spelled more ways than three patterns cover. Treat these rules as a guard against the common case and keep the real gate in CI, running the same PHP-CS-Fixer, PHPStan, Deptrac and PHPUnit commands as the hooks. None of this setup is agent-specific. The same hooks stop you at 6 p.m. on a Friday. --- If this was useful The Deptrac job is the one I would keep if I could keep only one. Formatting is cosmetic and PHPStan catches local mistakes, but a domain class that quietly starts depending on the framework is the change that makes a codebase hard to move years later. Decoupled PHP is my book on drawing those layer lines in a PHP app, so a tool like Deptrac has rules worth enforcing. If you run several agents on one repo, Hermes IDE gives each task its own worktree and runs your setup recipe first. It is free to download, source-available under BSL 1.1, runs on macOS, Windows and Linux, and is built in the open on GitHub. ![Decoupled PHP — Clean and Hexagonal Architecture for Applications That Outlive the Framework](amazon.com) Available on Kindle, Paperback, and Hardcover. English, German, and Japanese editions out now — Portuguese and Spanish coming soon.

  3. lefthook manages git hooks from one lefthook.yml in the repo. Hooks run in parallel, and you can point a linter at just the staged files instead of the whole codebase. Single Go binary, so no Node needed just to run a pre-commit. evilmartians/lefthook

    1 like · 156 views
  4. The font looks pretty good, and its compactness seems promising. But with my low screen resolution and high-contrast color scheme, it starts to strain my eyes when reading large blocks of text. I tried various fonts and settled on Martian Mono VF (evilmartians/mono). At a width of 0.6, it’s quite comfortable to read.

  5. @andrey_sitnik Супер! Вижу скрипт с командами в скилле можно его прикрутить к маковской folder action, бросать на папку видос и получать сконвертированные видосики github.com/evilmartians/agent-skills/blob/075cf6a…

    1 like · 18 views
  6. Reddit · · r/ffmpeg · u/gretchenfitze · evilmartians/agent-skills

    One ffmpeg script for web video: AV1/HEVC/H.264 sources or VP9/HEVC with alpha, a poster frame, and the <video> tag One script around ffmpeg for putting a clip on a website so it plays in every browser at the smallest size each one can take (with descent quality). Feed it a video, it writes x264, x265 and SVT-AV1 mp4s and a poster frame, and prints a <video> tag with the sources smallest first and the codecs strings filled in. Inputs with transparency get VP9 + HEVC with alpha instead (the HEVC one is done by avconvert on a Mac, ffmpeg can't write it). Defaults: x264 crf 26, x265 crf 28, both veryslow. svt-av1 qp 45 preset 1 with opus. vp9 crf 30 for the alpha case. -map_metadata -1 and faststart on everything. MIT, ~380 lines of bash: github.com/evilmartians/agent-skills/blob/main/sk… Disclaimer: built and maintained by Evil Martians, for the videos on evilmartians.com. There's a SKILL.md next to it so coding agents can run it, but the script doesn't depend on that.

    5 points
Trending awards, every completed day and week
Gold
01st place finishes
Silver
02nd place finishes
Bronze
03rd place finishes
Awarded repositories
0Owned by evilmartians now

No trending awards yet

None of the repositories evilmartians owns has finished in the top three of a completed trending day or week, by stars, forks or PR authors. See who is trending now.

A repository earns an award for finishing first, second or third in a completed trending day or week, ranked by new stars, new forks or pull request authors. Awards follow a repository through renames and transfers and count toward its current owner. Weeks run Monday to Sunday, UTC.