Changelog¶
All notable changes to this project are documented here.
[3.5.1]¶
Fixed¶
The parallel dashboard no longer repeats completed-job lines on a TTY without ANSI support. The live dashboard redraws in place with cursor-up escapes. On a stream that reports as a TTY but does not honour them — NO_COLOR, TERM=dumb or unset, IDE output panels — the cursor never moved, so each finished job's line was re-printed once per ~200 ms refresh tick (a fast job could stack dozens of copies while a slow one kept running). The live dashboard is now gated on ANSI decoration as well as the TTY check, so those streams degrade to the clean append-only renderer — one line per job:
$ NO_COLOR=1 githooks flow qa # a TTY that does not support ANSI
⏳ phpcpd...
⏳ phpstan-src...
phpcpd - OK. Time: 0.31s
phpstan-src - OK. Time: 1.06s
...
Results: 8/8 passed in 9.20s ✔️
Color-capable terminals are unaffected — the live, in-place dashboard is preserved. See Interactive parallel dashboard.
[3.5]¶
Added¶
--stats-sort=name|type — order the --stats table. With many similarly-named jobs the table came out in completion order (non-deterministic under processes > 1). Sorting groups related jobs and adds a # column with the real execution order:
$ githooks flow qa --stats --stats-sort=name
| # | Job | Status | Time |
| 2 | phpcs_app | OK | 10ms |
| 4 | phpcs_src | OK | 11ms |
| 1 | phpstan_app | OK | 10ms |
Structured formats (JSON/JUnit/SARIF) stay in execution order; JSON v2 gains a per-job executionOrder. See githooks flow and JSON v2 per-job fields.
Native commit-msg job — declarative commit-message validation. An inline job type (no shell spawned) that checks the commit subject against declarative rules or the conventional-commits preset, wired to Git's commit-msg hook. It replaces hand-written bash hooks and is validated by conf:check:
$ git commit -m "Add stuff."
✗ commit-msg: subject failed rule 'pattern'.
Subject: Add stuff.
Reason: Use Conventional Commits: tipo(scope?)!?: descripción.
Example: feat(api): add user endpoint
Configure with 'commit-msg' => ['commit-format'] plus a commit-format job (preset or custom rules). Upgrade note: reinstall hooks with githooks hook so the script forwards Git's message-file argument. See Commit Message Validation.
--diag — runtime diagnostics for CI post-mortems. Prints a machine snapshot (version, platform, CPU/cgroup, system memory, load, start timestamp) before the run, so a hung CI job shows whether PHP was blocked or starved. Auto-on in CI, opt-in locally:
$ githooks flow qa --diag
githooks 3.5.0 · linux · cpus=20 (cgroup limit: 20) · mem=3791 MB / 8096 MB · load=1.6 / 1.9 / 1.9 · 2026-06-03T23:17:05+00:00
Settings:
...
JSON v2 also gains a runtime node and absolute startedAt/endedAt per job and flow (always present, independent of --diag). See Runtime diagnostics and absolute timestamps.
--format=claude-code — AI agent stop-hook output. Emits the Claude Code stop-hook protocol on flow/flows/job. On success it is silent and exits 0; on a QA failure it prints one block line aggregating the failed jobs and still exits 0 (the protocol only honors the block on a zero exit):
$ githooks flow qa --fast-dirty --format=claude-code
{"decision":"block","reason":"## phpcs\nsrc/Foo.php:12 line too long\n\n## phpstan\nsrc/Bar.php:8 undefined variable $x"}
Pairs with --fast-dirty (3.4) to close the agent loop; a genuine config error still exits 1. See AI Agent Hooks (Claude Code).
Fixed¶
other-arguments is now honored by custom jobs in simple mode (without paths). The verbatim script form silently dropped other-arguments, so the extends + other-arguments pattern built the identical command for every variant. It is now appended after the script:
$ githooks flow qa --dry-run # three Jest shards sharing a base via extends
jest_ci_shard_1
yarn tests:ci --shard 1/3
jest_ci_shard_2
yarn tests:ci --shard 2/3
jest_ci_shard_3
yarn tests:ci --shard 3/3
See custom tool — simple mode.
[3.4.1]¶
Fixed¶
-
Unknown CLI options no longer silently shift the parser in
flow,flowsandjob. The three execution commands keepignoreValidationErrors()on so thatjob <name> -- <args>keeps forwarding extra args to the underlying tool, but a typo such asflow qa --foo=bar --config=/path/x.phpwould previously make Symfony swallow--fooand silently drop--config, then fall back toqa/githooks.php— wrong config, wrong jobs, no error. A newValidatesUnknownOptionsBeforeDashDashconcern now inspects the input tokens before either command reads--config, rejects unknown long options and short shortcuts (cluster-aware), and emits a Symfony-styleThe "--foo" option does not exist.per offender;flowandflowsadditionally emit a custom error if--itself is present (neither command supports passthrough). The new behaviour caps any input-validation typo at exit 1 before the configuration file is resolved, so a typo can no longer accidentally execute the project-wide QA flow. -
script-typed jobs now report their job key in OK/KO/SKIP logs instead of the executable path.ScriptJob::getDisplayName()historically overrode the parent and returned$this->executable, so two parallel jobs oftype: scriptsharing the sameexecutable-path(e.g. two shards invoking the same./run-testsrunner with differentother-arguments) printed two identical./run-tests - OK. Time: …lines, making them indistinguishable in the dashboard, JSON v2 dry-run and stats. The override was undocumented and inconsistent with every other Job type (Phpstan, Phpunit, Phpcs, Phpmd, Psalm, ParallelLint, Rector, PhpCsFixer, Phpcpd, Paratest, Custom, all of which inheritJobAbstract::getDisplayName()returning$this->name). Removed. The JSON v2 envelope is unaffected — itsname/typefields come fromgetName()/getType(), notgetDisplayName().
[3.4]¶
Added¶
-
--fast-dirtyexecution mode. Fourth execution mode targeting the unified working tree: tracked files modified vsHEAD(staged or unstaged, excluding deletions) ∪ untracked files honouring.gitignore. Fills the gap between--fast(staged only) and--fast-branch(branch diff). Designed for AI agentic hooks (Claude Code, Cursor, Cline, Copilot agent…) — the agent touches files without staging and we want the same pre-commit flow with--format=json. Available as--fast-dirtyonflow/flows/joband asexecution: 'fast-dirty'in flow/job declarations. Mutually exclusive with--fast/--fast-branch/--files/--files-from. Clean working tree → accelerable jobs skipped, exit 0 (no fallback tofull). See Fast-dirty mode. -
Intra-flow dependencies with
needs: [<job>, ...]. A flow entry can declare other jobs in the same flow it depends on:Jobs wait until all their'qa' => [ 'jobs' => [ 'yarn_install', ['job' => 'eslint', 'needs' => ['yarn_install']], ['job' => 'prettier', 'needs' => ['yarn_install']], 'phpstan_src', // independent — parallel with yarn_install ], ],needscomplete successfully; skip propagation is visible end-to-end (needs X failed,needs X was skipped). The TTY dashboard gains a⏸ jobName (waiting X, Y)lane and JSON v2 emitsneeds: [...].conf:checkvalidates the DAG statically (cycles, missing references, duplicates, empty list). Behaviour change:fail-fastnow lets jobs inrunningfinish naturally instead of terminating them — it cancels pending work, not in-flight work. See Job dependencies (needs). -
Per-flow execution mode by branch with
on => [branch_pattern => attrs]. A flow picks its execution mode based on the current branch:The execution mode lives inside the flow declaration, so a single CI step ('ci' => [ 'on' => [ 'master' => ['execution' => 'full'], '*' => ['execution' => 'fast-branch'], ], 'jobs' => [/* ... */], ],script: vendor/bin/githooks flows ci) covers both protected and feature branches without branch-aware conditionals in the CI definition or duplicated jobs. Pattern matching is first declared wins. Branch detection cascades from a new--branch=Xflag ongithooks flowthrough$GITHOOKS_BRANCH, CI env vars andgit rev-parse. TheSettings:header reportsmode = X (flows.<X>.on)when the branch match wins. See Branch-driven execution mode (on). -
Declarative per-flow-entry admission with
only-files/exclude-files. Flow entries inflows.<X>.jobsnow accept the existing string form or an object{job, only-files?, exclude-files?}that gates whether the job runs based on the change set, independently of the job's ownpathsfiltering. The decision is binary (skipped: truewithskipReasonvs run) and applies to all job types. Infullmode the rules are no-op. Replaces thetype: custom+git diff … grep -qE …; exit 0workaround that surfaces aspassedand breaks on POSIX-less runners. Same glob semantics as hook-levelonly-files. Combined withon(above), the flow declaration decides both which mode runs per branch and which jobs are admitted per change set — the CI pipeline stays a singleflowsinvocation without branch-aware conditionals or per-job rules duplicated in the CI YAML, and the admission logic is exercised the same way locally and in CI (GitLab CI'srules:/changes:and GitHub Actions'spaths:filters can be coarse and pipeline-dependent). See Per-entry admission rules.
Fixed¶
-
executable-prefix,fast-branch-fallbackandreportsnow cascade per-key fromflows.optionswhen a flow declares its ownoptionsblock. When a flow declared its ownoptionsto override an unrelated key (e.g.processes), the three keys above were read block-level instead of per-key and silently dropped their global value. Now they inherit per-key likefail-fast/processes/time-budget/memory-budget/allocator/statsalready did. See Per-key cascade. -
GitLab CI / GitHub Actions sections no longer leak raw tool JSON when
--format=codeclimateor--format=sarif(orreports.codeclimate/reports.sarifin config) is active. Structured formats reconfigure each tool to emit JSON so the file-based formatters can parse it; the side effect was that failing jobs printed the raw JSON blob as the visible body of their CI section. A new humanising display layer translates the per-tool JSON into a familiarfile line N message [rule]listing while the raw payload stays available unchanged for file-based reports and JSON v2output. See Human-readable KO body. -
githooks job <name> --format=jsonnow reflects the job's declaredexecutionin the envelope. When a job declaredexecution: fast/fast-branch(or the newfast-dirty), the JSON v2 envelope still reportedexecutionMode: "full"/source: "default"even though the file-set filtering already honoured the declared mode — CI dashboards and AI consumers read the wrong mode. TheexecutionModevalue and itseffectiveOptionssourceline now reflectjobs.<name>.execution.
[3.3.3]¶
Fixed¶
- Fast-branch / fast no longer fail with spurious "no files" errors when a job's tool config strips every input via its internal exclusion list. Repro: a branch touches only files under one subtree (e.g.
src/foo/...); the wrapper hands those files to a job whose.neondeclaresexcludePaths.analyse: [src/foo](PHPStan) or whose--ignoreCSV covers them (PHPCS). The tool drops 100 % of the input and exits non-zero with[ERROR] No files found to analyse.(PHPStan, exit 1) orERROR: All specified files were excluded or did not match filtering rules.(PHPCS, exit 16 on older versions and the PHPCSStandards fork). Before this fix the wrapper reported the job as failed, breaking MRs in projects that split coverage across complementary jobs. PHPStan and PHPCS now recognise these "empty after filtering" exits, reinterpret them asskipped: truewithskipReasoninstead ofsuccess: false, and bypass threshold evaluation (the tool didn't do real work, so timing it would be meaningless). PHPMD already tolerates this case natively (exit 0when its--excludeempties the set); the other accelerable tools (parallel-lint, psalm, rector, php-cs-fixer) silently ignore non-matching inputs and do not need an override.
[3.3.2] ⚠️ Do not use — broken release¶
This release is functionally identical to 3.3.1. The git tag v3.3.2 was published against a master commit whose bundled .phar binaries (builds/githooks, builds/php7.4/githooks) had never been updated from the rc-3.3.2 branch where CI compiled them. Since GitHooks runs as a standalone .phar, installing wtyd/githooks:3.3.2 ships the v3.3.1 binary under the v3.3.2 tag name. The fixes listed below are present in the source code of the tag but not in the executed binary.
Use v3.3.3 — same fixes, correctly bundled.
Fixed¶
- Code Climate and SARIF reports requested via
flows.options.reports.codeclimate/reports.sarifin config or via--report-codeclimate=PATH/--report-sarif=PATHCLI flags came out empty ([]/ no findings) when the primary--formatwas anything other thancodeclimate/sarif. The flag that asks each tool for JSON output only activated on--format=codeclimate|sarif, so every tool ran with its default human-text format and the report parsers (which all dojson_decode()over stdout) found nothing to extract. Affects every tool with a JSON-dependent parser: PHPStan (--error-format=json), PHPCS (--report=json), PHPMD (positionaljsonformat), Psalm (--output-format=json) and parallel-lint (--json). Fixed: tool-level JSON output is now requested whenever a codeclimate or sarif payload will be produced, regardless of how it was requested.
Improved¶
- JUnit
<failure>payloads now pretty-print embedded JSON so GitLab/Jenkins viewers render each finding on its own indented block. PHPMD already emitsJSON_PRETTY_PRINTnatively; PHPStan/PHPCS/Psalm/parallel-lint emit compact one-liner JSON. When a pipeline triggers tool JSON output (typical GitLab setup pairs JUnit + Code Climate), the JUnit<failure>arrived as a single 1000+ char line. The formatter now detects a parseable JSON span inside<failure>and re-encodes it with indentation; non-JSON outputs (custom jobs, scripts) and JSON with prologue/epilogue (PHPStan's "Instructions for interpreting errors" stderr block) are preserved verbatim except for the JSON span itself. Idempotent for already-pretty payloads (semantics preserved; bytes may differ becauseJSON_UNESCAPED_SLASHESturns\/into/).
[3.3.1]¶
Fixed¶
--fast/--fast-branchno longer leave non-accelerable jobs (phpunit,paratest,phpcpd,script,custom,composer-*) running their full suites when the effective input set is empty (no staged files / no diff vs base). The skip is now universal: any job — accelerable or not, with or withoutpathsdeclared — is skipped with reasonno changes to validatewhen the mode produced no input. Restores parity with the v2.x contract ("nothing changed = nothing to run").cache:clearnow resolves the effective cache path for each job instead of relying on hard-coded defaults. Previously the command read a regex on the top-level.neon(PHPStan, ignoringincludes:and placeholders) and used hard-coded literals for every other tool —cache:clearsilently reported "not found" while the real cache lived elsewhere. After the fix:- PHPStan:
tmpDir:is followed throughincludes:recursively (cycle-safe) and%currentWorkingDirectory%/%rootDir%are expanded. - Psalm: reads
cacheDirectoryfrompsalm.xml, resolved relative to the XML. - PHPCS: reads job arg
cache, then<arg name="cache" value="..."/>from the ruleset. - PHPUnit: reads
cacheResultFileandcacheDirectory(10+) fromphpunit.xml/phpunit.xml.dist. - Rector: best-effort regex over
cacheDirectory(...)inrector.php(literal,__DIR__ . '/literal',sys_get_temp_dir() . '/literal'). Default fixed tosys_get_temp_dir() . '/rector_cached_files'(was incorrect/tmp/rector, also non-portable on Windows). - PHP-CS-Fixer: best-effort regex over
setCacheFile(...)in.php-cs-fixer.php; respects job argcache-fileover the config (matching what php-cs-fixer itself does). - PHPMD: default fixed to
.phpmd.result.cache(was incorrect.phpmd.cache).
- PHPStan:
- When Rector / PHP-CS-Fixer config uses a dynamic expression for the cache path (variable, helper, env),
cache:clearfalls back to the default and surfaces a warning explaining why and how to override. Last-resort meta-argcache-diron the Rector job lets users force a path; PHP-CS-Fixer relies on its existingcache-filearg (which the tool itself respects as--cache-file). Seecache:clear. - PHPUnit cache precedence: when
phpunit.xmldeclared bothcacheResultFile(legacy) andcacheDirectory(PHPUnit 10+), GitHooks picked the legacy one and ignored the modern attribute. PHPUnit itself does the opposite —cacheDirectorywins. Users migrating to PHPUnit 10 with both attributes for transitional reasons sawcache:cleardeleting the wrong path. NowcacheDirectorytakes precedence, withcacheResultFileas fallback. - PHPStan
tmpDir:insideservices:was misread as the rootparameters.tmpDir. A NEON service constructor argument namedtmpDirwould be returned as the PHPStan tmpDir, leadingcache:clearto delete a path the user never declared as cache. The resolver now requirestmpDir:to live directly under a top-levelparameters:block. - Meta-args with whitespace-only values (
'cache-dir' => ' ','cache-file' => ' ','cache' => ' ') were accepted as literal paths. Now trimmed before validation; whitespace-only falls back to the default rather than producingrm -rf " ".
[3.3.0]¶
New Features¶
Combined flow runs (flows command + meta-flows)¶
A new flows command runs several flows in a single plan — one PHP runtime, one shared thread budget, one combined FlowResult — replacing the typical "two CI steps that each spin up composer install" pattern with a single invocation.
- Four invocation modes (auto-detected from the args):
flows qa(single normal flow) — equivalent toflow qa, identicalFlowResult.flows qa lint(≥ 2 normal flows) — ad-hoc combination, jobs deduped by first-occurrence order.flows ci-pack(1 meta-flow declared in config) — declarative composition with the meta-flow's own options.flows ci-pack deploy(mixed) — meta-flow + extra flows, where per-flow / per-alias options are deliberately ignored.- Meta-flows in config (
flows.<alias>.flows): a flow that lists other flows instead of jobs. Declares its ownoptionsandreports.conf:checkvalidates the new shape: eachflows.<X>declares exactly one ofjobsorflows, references must point at existing normal flows (no nesting in v3.3), and the jobs/flows/meta-flows namespace must stay flat. flows.optionscascade per key:cli > flows.<X>.options > flows.options > default(single-flow / declarative) collapses tocli > flows.options > defaultfor ad-hoc and mixed runs (per-flow/alias options are intentionally ignored — see why).- Ignored-options warning: when a flow or alias's
optionsblock is ignored because of the run mode,flowsemits a one-line notice naming the ignored sources so the operator knows what is and isn't being applied.
Cross-cutting conditions header + effectiveOptions¶
A new conditions header is emitted at the start of every flow, flows and job run to make the active options visible at a glance:
Settings:
processes = 4 (cli)
fail-fast = true (flows.ci-pack.options)
mode = full (default)
time-budget = none (default)
memory-budget = none (default)
allocator = fifo (default)
stats = false (default)
Flows: qa, lint
Every row carries its (source) parenthesis — (default) included — so the column stays aligned and the audit trail is complete.
- Channels: stdout in text mode (default); stderr when a structured format is combined with
--show-progress. Silent for plain--format=json|junit|sarif|codeclimateso stdout payloads stay clean. - JSON v2 contract (always present in
flow/flows/jobruns): a new rooteffectiveOptionsblock listing each option'svalueandsource.source∈{cli, flows.<X>.options, flows.<alias>.options, flows.options, default}. flows[]root field (multi-flow only): the list of normal flows actually executed after meta-flow expansion. Absent inflow Xand single-flow degenerate runs (so existing single-flow consumers ignore it).
Both fields are additive. Consumers that read v2 today keep working unchanged; modern consumers (CI dashboards, AI tools) can now show the precise option resolution without cross-referencing config + CLI.
Files mode (--files / --files-from / --exclude-pattern)¶
flow and job accept three new flags that drive a flow against an explicit list of files supplied by the user. Covers IDE on-save (single-file analysis), CIs with shallow checkouts where --fast-branch cannot compute a diff, and any external tool that already produced a list of paths.
--files=a,b,c— CSV. Paths resolve against CWD; absolute paths are accepted as-is. Directories expand recursively to.php/.phtml.--files-from=PATH— manifest with one path per line. Comments (#), blanks, CRLF and UTF-8 BOM are tolerated. Use this to bypass the shellARG_MAXlimit (git diff --name-only origin/main...HEAD > /tmp/changed.txt && githooks flow qa --files-from=/tmp/changed.txt).--exclude-pattern=glob1,glob2— drop matching paths from the input list (post-expansion). Same glob syntax as hook config (*,**,?). Requires--files/--files-from.- Behaviour: accelerable jobs (phpstan, phpcs, phpcbf, phpmd, psalm, parallel-lint, php-cs-fixer, rector, custom with
accelerable: true) run only on the intersection of input files and their configuredpaths; jobs with no match are skipped with reason"no input files match its paths". Non-accelerable jobs (phpunit, phpcpd, composer-*, script) ignore the list and run with their originalpaths. - JSON v2: when files mode is active,
executionModeis"files"and a new rootinputFilesblock plus per-jobinputFilesslice (accelerable jobs only) are emitted. Backward-compatible — fields are absent in the legacy modes. - Mixing:
--files/--files-fromwin over--fast/--fast-branchwith a warning. The two file flags are mutually exclusive. - CLI-only:
conf:checkrejectsfiles/files-fromkeys declared inflow.optionsor in a job (volatile by design). See How-To: --files / --files-from.
Multi-report (reports / --report-*)¶
PHPUnit-style multi-report: a single flow (or job) run can emit several report files at once instead of being executed once per format. Pipelines that need SARIF (Code Scanning) plus JUnit (test dashboards) plus Code Climate (GitLab MR widgets) no longer have to re-analyse everything 3 times.
- CLI flags:
--report-json=PATH,--report-junit=PATH,--report-sarif=PATH,--report-codeclimate=PATH. Each one writes the corresponding format to the given path. Combine as needed. - Declarative config: new
reportsmap underflows.optionsand per-flowoptions: - Precedence: CLI overrides config format by format.
--report-sarif=other.sarifonly overrides the SARIF entry; other formats keep config values. --no-reports: PHPUnit--no-coverage-style flag that skips the configreportssection without cancelling CLI--report-*flags. Lets a consumer (an AI tool, an ad-hoc script) read clean JSON from stdout without dropping side-effect files declared by the project's config:--formatis unchanged: still governs stdout the same way as in 3.2.--format=sarif --report-sarif=foo.sarifis legal and produces both stdout SARIF and the file.conf:checkvalidation: rejects unsupported format keys, non-string paths and unwritable target locations; warns when the parent directory does not exist (it gets created on run).
Performance monitor — flow time-budget + per-job warn-after / fail-after¶
Two parallel, independent systems that watch the temporal health of every QA run:
- Per-job thresholds (
jobs.<name>.warn-after/fail-after, seconds): catch local regressions of a specific job. Crossingwarn-afterannotates⚠; crossingfail-afterflips a passing job to KO with exit1. - Flow time-budget (
flows.options.time-budgetorflows.<name>.options.time-budget): catch accumulated drift across the whole flow. The post-hoc sum of executed-job durations is compared withwarn-after/fail-afterdeclared at the flow level. A flow that crossesfail-afterexits 1 even when every job passed individually — the conceptual key of the feature. - Independence: declaring
time-budgetat the flow level does NOT propagatewarn-after/fail-afterto individual jobs. The two layers answer different questions ("is this job regressing?" vs. "is the pipeline as a whole regressing?") and remain decoupled. - CLI overrides:
--warn-after=N,--fail-after=N(flow-level onflow/flows; job-level onjob).--no-time-budgetdisables both layers for that run; mixing it with--warn-afteremits a warning on stderr. - JSON v2 (explicit-null pattern): a new root
timeBudgetfield (object ornull) and per-jobthresholdfield (object ornull) are always present. Consumers can writeif (job.threshold) { … }without existence checks.reasonis a string when warned/failed istrue,nullotherwise. - Conditions header: extended with a
time-budget=...segment showing the effective values and their origin (flows.options,flows.<X>.options,cli,default). conf:checkvalidation: rejects non-positive integers,warn-after >= fail-after,time-budgetplaced inside a job; warns on unknown keys with did-you-mean suggestions.
Memory budget + 2D allocator + RSS sampler (Linux)¶
GitHooks now declaratively watches RSS consumption per job and across the whole flow, schedules admissions in 2D (cores + memory) when both axes are constrained, and surfaces peaks in a canonical --stats table — none of GrumPHP, CaptainHook, lefthook, pre-commit or golangci-lint expose this combination.
- Per-job memory threshold (
jobs.<name>.memory): two equivalent forms. - Short form
memory: 2000(MB) — single warn threshold AND scheduler reservation when a flowmemory-budgetis declared. - Extended form
memory: { warn-above: 1500, fail-above: 2000 }— explicit thresholds, no reservation. - Crossing
warn-aboveannotates⚠; crossingfail-aboveflips the job to KO with exit1even when the tool itself returned0. - Flow
memory-budget(flows.options.memory-budgetor per-flow): observational watchdog over the simultaneous RSS sum across jobs in flight. Crossingfail-abovekills jobs in flight (process->stop(0)) and skips the queued ones with reason"flow memory-budget exceeded". The flow exits 1 even if every individual job had passed (the conceptual key of the feature). - 2D allocator (
flows.options.allocator: fifo|greedy): when amemory-budgetis declared and at least one job has a short-formmemory:reservation, the pool admits jobs only when both cores and memory fit. FIFO blocks the entire queue when the head does not fit; greedy scans for the first fitting job. 1D mode (cores only) is preserved when either side of the precondition is missing. - RSS sampler: Linux via
/proc/<PID>/statuswalked across the process tree (root + descendants — Symfony's shell wrapper alone is ~1 MB; the actual analyzers are children); macOS via a singleps -o pid=,ppid=,rss= -axinvocation per tick. Polled every 1 second while jobs are in flight. Windows degrades gracefully — a short stderr warning (⚠ Memory budget disabled: RSS sampling not available on Windows) disables thresholds; the 2D allocator still schedules from declaredmemory:reservations and--statsstill emits the cores axis. --statstable: 5-column summary (Job / Status / Time / Peak Cores / Peak Memory) with a TOTAL row + temporal attribution linesMemory peak at Xs: jobA Pmb + jobB Pmb...andCores peak at Xs: jobA + jobB.... Active when--stats(CLI) orstats: true(config).- CLI overrides:
--memory-warn-above=N,--memory-fail-above=N,--no-memory-budget,--allocator=fifo|greedy,--stats. Apply flow-level except ingithooks jobwhere they apply to the single job. - JSON v2: new root-level
memoryBudgetandstatsblocks (always present under the explicit-null pattern), per-jobmemoryReserved,memoryPeak,memoryThresholdandkilledReason. SARIF / JUnit / Code Climate are unchanged in this iteration. - Conditions header: extended with
memory-budget=warn-above=WMB,fail-above=FMB (origin),allocator=fifo|greedy (origin)andstats=true|false (origin)segments alongsidetime-budget. conf:checkvalidation: positive-integer guards, warn/fail ordering,memory > memory-budget.warn-above(could-never-run), unknown allocator values,memory-budgettypo suggestions.
cores ↔ native thread flag interchangeability¶
cores: N and the tool's native threading flag (parallel on phpcs/phpcbf, threads on psalm, jobs on parallel-lint, processes on paratest) now work identically in both directions: declaring either one reserves N cores in the budget and emits the right CLI flag at runtime. Until v3.3 declaring only the native flag (without cores) was silently dropped in parallel mode and the allocator distributed the budget evenly instead.
- No config change required: the existing pattern of pinning with
cores: Nkeeps working unchanged. conf:checkwarning — single-threaded tools: declaringcores > 1onphpmd,phpunitorphpcpdnow emits a warning ("<tool>is single-threaded;coresreserves slots in the budget without benefit"). The tool only uses one core, so reserving more slows admission of other jobs without gain.cores: 1and absence ofcoresare silent.type: customis exempt — user scripts may have their own concurrency the system can't inspect.conf:checkfix — phpcbf: the conflict warning betweencoresandparallel(already emitted for phpcs) now applies to phpcbf as well.conf:checkvalidation — native flag: when declared withoutcores, the native threading flag (parallel/threads/jobs/processes) is validated as a positive integer — symmetric withcores. Aparallel: -1orthreads: '4'now warns instead of silently degrading at the allocator.- Symmetric clamp: a native flag value >
processesis clamped to the budget at runtime, the same waycores: N > processeswas clamped before. - The flow rules — args clamp at every path: until v3.3 a job declaring more cores than the flow's
processesbudget still spawned its declared workers in the SO (the pool's accounting was clamped, butargs['parallel']/args['threads']/ etc. were not). Same job in two flows ("local" withprocesses: 4, "ci" withprocesses: 16) had to choose one of the two budgets in its declaration. NowapplyThreadLimit()clamps the override to the flow's budget before reaching the tool, in both the explicit-override and the sequential-default paths. Declare the maximum your job can use; each flow caps it. conf:checkcross-flow warning for uncontrollable jobs: phpstan reads its workers from.neonand custom jobs are opaque scripts — GitHooks cannot force either to honour the flow budget at runtime. Whenphpstan.maximumNumberOfProcesses(read from the configured.neon) orcores: Ndeclared on atype: customjob exceeds a flow'sprocesses,conf:checknow emits a warning per affected flow naming both values and explaining that other jobs will wait in serial while the offending one runs. Same job referenced by multiple flows is validated against each flow's budget independently; flows that fit are silent. Warning, not error — the user may know their machine can absorb it.
Deprecations¶
kebab-case keys for jobs.<name> (step 1 of 3)¶
The four legacy camelCase keys inherited from v2 inside jobs.<name> are deprecated in favour of their kebab-case counterparts. Both forms keep working in v3.3.x; the camelCase forms will be removed in v4.0.
| camelCase (deprecated) | kebab-case (canonical) |
|---|---|
executablePath |
executable-path |
otherArguments |
other-arguments |
ignoreErrorsOnExit |
ignore-errors-on-exit |
failFast |
fail-fast |
- Runtime warning: every command that loads the config (
flow,flows,job,conf:check,system:info) emits aDeprecated: 'X' is renamed to 'Y'. Will be removed in v4.0.line on stderr per camelCase key found. - Structured output: a new root-level
deprecations[]block in JSON v2 (andruns[0].properties.deprecationsin SARIF) lists each detection as{job, oldKey, newKey, removalVersion, kind}. As a side-effect, the JSON v2 also gains a rootwarnings[]field (always present, empty when no warnings) — useful for CI dashboards and AI consumers. - Conflict: declaring both forms for the same key in the same job aborts that job with an error (
conflicting keys '...' and '...'). Pick one. - Out of scope for v3.3:
conf:migrateis not updated yet — that is step 2 of the deprecation plan, in a later v3.x. The camelCase removal itself is step 3, in v4.0.
Migration guide: Migration → v3.3 deprecations.
[3.2.0]¶
New Features¶
Redesigned output system¶
The output behaviour now depends on the format and the execution context. The unifying rule: the format decides whether the output streams live or is buffered and emitted at the end.
- Live streaming in
githooks job X(single job): tool output (phpstan, phpcs, etc.) is now streamed in real time instead of buffered. Long-running jobs (phpmd, phpunit with coverage) no longer look frozen — you see the tool's actual progress as it happens. - Live streaming in
githooks flowwithprocesses=1: each job is streamed with a header separator between jobs (likemakeordocker compose up). You see each tool's output as it runs instead of onlyOK/KOlines at the end. - Interactive parallel dashboard in
githooks flowwithprocesses > 1: when running in a TTY, the output upgrades to a live dashboard with three states — ⏺ queued, ⏳ running (with a live timer), ✓/✗ done. On completion it collapses to a clean summary. In non-TTY environments (CI, piped stdout) it falls back to append-only streaming text so logs remain parseable. Activated automatically viaposix_isatty(STDOUT); no flag needed. - stdout/stderr split for structured formats: for
json,junit,codeclimateandsarif, progress lines (OK job (Xms) [Y/Z],Done., colours) route to stderr and the structured payload stays on stdout. Enablesgithooks flow qa --format=json > report.jsonwithout contamination. - TTY-aware progress with
--show-progressoverride: the stderr progress handler only emits when stderr is a TTY, soflow|job --format=json | jq ...works off pipes, CI and agents without2>/dev/null. Pass--show-progressto force progress even off a TTY — useful for long-running CI pipelines.--dry-runnever emits progress.
Output formats¶
- JSON schema v2 (
--format=json): enriched per-job fields (type,exitCode,paths,skipped,skipReason,fixApplied) plus top-levelversion: 2,executionMode,passed,failed,skippedcounters. Stable contract for AI tools, CI dashboards and scripts. - JUnit
<skipped>support: skipped jobs now emit<skipped>elements with a reason attribute. - Code Climate format (
--format=codeclimate): GitLab-compatible Code Quality report. - SARIF format (
--format=sarif): SARIF 2.1.0 report for GitHub Code Scanning, Azure DevOps and other static-analysis consumers. - Unified output target for structured formats:
json,junit,codeclimateandsarifall print to stdout by default; pass--output=PATHto write the payload to a file. Shell redirection (> file) remains equivalent.
CI integration¶
- Native CI annotations (CI/CD Integration): auto-detects
GITHUB_ACTIONS=trueorGITLAB_CIand wraps job output in::group::/::endgroup::plus::error file=…,line=…::annotations (GitHub) orsection_start:/section_end:markers (GitLab). Parsesfile.php:LINEpatterns from tool output. --no-ciflag: opt out of the auto-detection when a CI env var is set but you want plain output (runningactlocally, custom CI where those markers aren't parsed, or scripting on top of GitHooks).
New native job types¶
- PHP CS Fixer (
type: php-cs-fixer): native support withconfig,rules,dry-run,diff,allow-risky,using-cache,cache-filekeywords. Accelerable. - Rector (
type: rector): native support withconfig,dry-run,clear-cache,no-progress-barkeywords. Accelerable. - Paratest (
type: paratest): first-class support for paratest, the parallel driver for PHPUnit. Inherits every PHPUnit keyword and addsprocesses(linked tocores).
Thread budget¶
- Per-job
coresreservation (cores): every job can declarecores: Nto reserve N slots in the thread budget. Controllable tools (phpcs, psalm, parallel-lint, paratest) automatically receive their native threading flag (--parallel,--threads,-j,--processes) with the same value, so you configure parallelism once per job regardless of the tool. Budget-only tools (phpstan, custom jobs) usecoresto keep the--monitorpeak accurate without forcing worker count.conf:checkwarns whencorescoexists with a tool's native threading flag.
Other¶
conf:checkcommand truncation: long generated commands are truncated to 80 chars (with…) in the job table to keep the output readable on narrow terminals.githooks job X --dry-runstill shows the full command.- All supported tools ship as dev dependencies:
brianium/paratest,friendsofphp/php-cs-fixer,rector/rectorandsebastian/phpcpdare now declared inrequire-dev, andpsalmis correctly stripped from the.pharat build time (it was being embedded by mistake). Runningcomposer installin the repo gives every supported tool a binary undervendor/bin/, and the distributed.pharno longer ships QA tools internally.
[3.1.0]¶
New Features¶
- Local override (
githooks.local.php): GitHooks looks for agithooks.local.phpfile alongsidegithooks.php. If found, its contents are merged over the main config usingarray_replace_recursive. Allows per-developer environment customization without modifying the shared config. Addgithooks.local.phpto.gitignore. See Docker & Local Override. executable-prefixoption: New option at global, flow, and job level. Prepends a command to all job executables (e.g.'docker exec -i app'). Per-job override with''ornullto opt out. Enables Docker, Laravel Sail, and remote environments from a single config. See Options: executable-prefix.- Extra arguments via
--forjobcommand:githooks job phpunit_all -- --filter=testFoopasses extra flags to the underlying tool. Enables dynamic execution from AI tools, scripts, or quick debugging without modifying configuration. Seegithooks job. - External documentation site: Full MkDocs Material site with getting started guide, configuration reference, CLI reference, tool docs, how-to guides, migration guides, and comparison page.
Bug Fixes¶
- Fix skipped job warnings not showing orange color in terminal output.
- Fix parallel execution deadlock when a job's reserved cores exceeded the total
processesbudget. The thread allocator now clamps both explicitcores: Noverrides and uncontrollable tools' default workers (e.g. PHPStan reading 4 from.neonwhileprocesses: 2) to the budget, so the admission queue can always admit the head instead of rejecting it forever and spinning the executor at 100% CPU.
[3.0.0] - 2026-04-10¶
Breaking Changes¶
- PHP minimum raised to 7.4. Dropped support for PHP 7.0-7.3.
- SecurityChecker tool removed. Use a
customjob withcomposer auditas replacement. - New configuration format: hooks/flows/jobs. Replaces the previous
Options/Toolsformat. The old format still works but emits a deprecation warning. toolcommand deprecated. Replaced byflowandjobcommands. Will be removed in v4.0.- YAML configuration deprecated. PHP format is now the primary format. YAML still works but emits a deprecation warning. Will be removed in v4.0.
New Architecture — Hooks, Flows, Jobs¶
- Hooks: Map git events (
pre-commit,pre-push, etc.) to flows and jobs. Usescore.hooksPathwith a universal script instead of copying files to.git/hooks/. - Flows: Named groups of jobs with shared options (
fail-fast,processes). Reusable across hooks and directly executable from CLI/CI. - Jobs: Individual QA tasks with declarative configuration. Each job declares a
type(phpstan, phpcs, phpunit, custom, etc.) and its arguments.
New Commands¶
githooks flow <name>— Run a flow by name. Supports--fail-fast,--processes=N,--exclude-jobs,--only-jobs,--dry-run,--format=json|junit,--fast,--fast-branch,--monitor.githooks job <name>— Run a single job by name. Supports--dry-run,--format=json|junit,--fast,--fast-branch.githooks hook:run <event>— Run all flows/jobs associated with a git hook event (called by the universal hook script).githooks status— Show installed hooks, their sync state with config (synced/missing/orphan), and target flows/jobs.githooks system:info— Show detected CPUs and currentprocessesconfiguration with budget warning.githooks conf:migrate— Migrate v2 configuration to v3 format with automatic backup.githooks cache:clear— Clear cache files generated by QA tools. Accepts job names, flow names, or a mix.
Updated Commands¶
githooks hook— Now usescore.hooksPath+.githooks/directory instead of copying scripts to.git/hooks/.--legacyflag preserves old behavior (Git < 2.9).githooks hook:clean— Default now removes.githooks/+ unsetscore.hooksPath.--legacyflag removes individual hooks from.git/hooks/.githooks conf:init— Now supports--legacyflag to generate v2 format.githooks conf:check— Updated for v3: shows Options, Hooks, Flows, and Jobs tables with the full command each job will execute. Deep validation: verifies executables exist, paths are valid, and config files are accessible.
New Job Types¶
- Custom: Replaces the v2
scripttool. Supportsscriptkey (simple mode) and a new structured mode viaexecutablePath+paths+otherArguments. Structured mode enables--fastacceleration identical to standard tools.
Execution Modes and Structured Output¶
--format=jsonand--format=junit: Structured output forflowandjobcommands. JSON for machine-readable results; JUnit XML for CI test reporting.fast-branchexecution mode: New third mode alongsidefullandfast. Analyzes files that differ between the current branch and the main branch. Ideal for CI/CD. Non-accelerable jobs always run with full paths. Per-jobaccelerablekey overrides the default. Deleted files are excluded automatically.fast-branch-fallbackoption: Controls behavior whenfast-branchcannot compute the diff (e.g. shallow clone). Values:full(default) orfast.main-branchoption: Configure the main branch name forfast-branchdiff computation. Auto-detected if not specified.- Thread budget:
processesnow controls total CPU cores, not just parallel jobs. GitHooks distributes threads across jobs respecting each tool's capabilities (phpcs--parallel, parallel-lint-j, psalm--threads). PHPStan workers detected from.neonconfig. --monitorflag: Shows peak estimated thread usage after flow execution, with warning if budget was exceeded.- Job argument validation:
conf:checkandflow/jobcommands validate job configuration keys and types at parse time.
Developer Experience¶
--dry-runflag: Shows the exact shell command each job would execute without running anything. Works with all output formats —--format=jsonincludes acommandfield per job.--only-jobsflag: Inverse of--exclude-jobsfor theflowcommand. Run only the specified jobs:githooks flow qa --only-jobs=phpstan_src,phpmd_src.- Deep validation in
conf:check: Checks that executables exist, that configuredpathsare real directories, and that referenced config files are accessible. - Auto-detection of
executablePath: When omitted, GitHooks looks forvendor/bin/{tool}before falling back to system PATH.
Conditional Execution¶
exclude-files: Excludes staged files matching glob patterns from triggering execution. Always prevails overonly-files.exclude-on: Excludes branches matching glob patterns. Always prevails overonly-on.- Double-star (
**) glob support: File patterns now support**for recursive directory matching.src/**/*.phpmatches all PHP files undersrc/at any depth. hooks.commandconfig key: Customize the command used in generated hook scripts (e.g.'command' => 'php7.4 vendor/bin/githooks').