Output Formats¶
GitHooks supports six output formats: text (default), json, junit, codeclimate, sarif and claude-code. All are available on the flow, flows and job commands via --format=FORMAT.
Text (default)¶
Human-readable output with status, time, and error details:
parallel_lint - OK. Time: 150ms
phpcs_src - OK. Time: 890ms
phpstan_src - KO. Time: 2.34s
phpstan_src:
/src/Foo.php:12 Access to undefined property $bar
Results: 2/3 passed in 3.45s
Live streaming for job and sequential flows¶
When you run a single job (githooks job X) or a flow with processes=1, each tool's output streams in real time. Long-running jobs (phpmd, phpunit with coverage) no longer look frozen — you see the tool's own progress as it happens.
For flow --processes=1, a header separator is printed between jobs (like make or docker compose up):
--- phpstan-src ---
[OK] No errors
phpstan-src - OK. Time: 715ms
--- parallel-lint ---
Checked 144 files in 0.2 seconds
parallel-lint - OK. Time: 196ms
Interactive parallel dashboard¶
When running a flow with processes > 1 in an interactive terminal (TTY), the text output upgrades to a live dashboard showing queue / running / done states with per-job timers:
⏳ phpstan-src [0.9s] ← running, live timer
⏳ parallel-lint [0.9s]
⏳ phpmd-src [0.9s]
⏳ phpcs [0.1s] ← just entered a freed slot
⏺ phpunit ← queued
⏺ composer-audit
On completion, the dashboard collapses to a clean summary.
Activation is automatic: the live dashboard turns on only when stdout is both a TTY and ANSI-decorated. No flag is needed. When stdout is not a TTY (CI, redirected stdout, pipes) or is a TTY without ANSI support (NO_COLOR, TERM=dumb or unset, terminals and IDE panels that don't render escape sequences), it falls back to append-only streaming text so logs stay parseable — the dashboard redraws in place with cursor escapes, which are no-ops on a stream that can't honour them and would otherwise re-print each completed line on every refresh tick.
--monitor is a separate feature
--monitor adds a thread-usage report at the end of execution (peak estimated threads, warning if the budget was exceeded). It is independent of the dashboard — you can combine them (--monitor on top of the dashboard) or use it in CI with the plain output.
stdout / stderr split¶
For all structured formats (json, junit, codeclimate, sarif):
- stdout carries the structured payload only — never mixed with progress, colours or skip notices.
- stderr carries progress lines (
OK job (Xms) [Y/Z],Done. X/Y completed.), colours, and any CI annotations — only when a TTY is attached or--show-progressis set.
Auto-suppress without a TTY¶
The progress handler detects whether stderr is a TTY. If it is not (pipe, subshell, CI agent, Claude, cron), no progress is emitted. stdout stays clean and ready to consume without any redirection:
# From a script, agent or pipe — stderr is naturally empty
githooks flow qa --format=json | jq '.jobs[] | select(.success == false)'
# Interactive terminal — stderr shows OK/KO while the flow runs
githooks flow qa --format=json > report.json
No need for 2>/dev/null
Earlier pre-releases required redirecting stderr to keep consumers happy. From 3.2.0 this is automatic via stream_isatty(STDERR) and the idiomatic UNIX pattern used by git, docker and npm.
Force progress with --show-progress¶
Long-running pipelines in CI can look stuck because stderr is silent by default. Pass --show-progress to force progress to be emitted even when stderr is not a TTY:
# In a CI job that takes several minutes
githooks flow qa --format=json --show-progress --output=report.json
# → stderr: OK phpcs_src (2.1s) [1/6], KO phpstan_src (5.3s) [2/6], …
# → report.json: clean JSON payload
--show-progress is a dedicated flag: it only affects progress emission, so stdout remains a valid JSON/JUnit/CC/SARIF document. The standard Symfony -v / --verbose flag is reserved for its original purpose (framework verbosity) and has no effect on progress output.
Dry-run emits no progress at all¶
--dry-run does not execute any tool, so there is nothing to measure. The progress handler is skipped entirely: stderr stays empty regardless of TTY or --show-progress, and stdout contains the structured payload with totalTime: "0ms".
Writing a report to a file¶
All four structured formats print to stdout by default. Pass --output=PATH to write the payload to a file, or use shell redirection — both are equivalent:
githooks flow qa --format=json --output=reports/qa.json
githooks flow qa --format=junit --output=reports/junit.xml
githooks flow qa --format=codeclimate --output=reports/qa-codeclimate.json
githooks flow qa --format=sarif --output=reports/qa.sarif
# Same result with shell redirection:
githooks flow qa --format=json > reports/qa.json
githooks flow qa --format=sarif > reports/qa.sarif
Pick the flag form when the surrounding tooling (pipeline DSL, script linter) prefers explicit arguments over shell glue; pick redirection when you are composing with tee, filters, or alternate stdout handling.
JSON v2¶
Machine-readable output for CI pipelines, scripts, and AI tools:
Schema¶
{
"version": 2,
"flow": "qa",
"success": false,
"totalTime": "15.18s",
"executionMode": "full",
"passed": 2,
"failed": 1,
"skipped": 0,
"timeBudget": {
"warnAfter": 800,
"failAfter": 1200,
"totalJobDuration": 1015.2,
"warned": true,
"failed": false
},
"memoryBudget": {
"warnAbove": 3000,
"failAbove": 5000,
"peakObserved": 3743,
"peakAtSecond": 12.34,
"peakAttribution": [{ "name": "phpstan_src", "value": 3500 }],
"warned": true,
"failed": false
},
"stats": {
"cores": {
"limit": 8,
"flowPeak": { "value": 8, "atSecond": 0.01, "jobsInFlight": ["phpstan_src", "phpcs", "phpunit"] }
},
"memory": {
"flowPeak": { "value": 3743, "atSecond": 12.34, "jobsInFlight": [{ "name": "phpstan_src", "value": 3500 }] }
}
},
"runtime": {
"githooksVersion": "3.5.0",
"platform": "linux",
"ci": "gitlab-ci",
"startedAt": "2026-06-02T09:14:07.512+00:00",
"endedAt": "2026-06-02T09:14:22.694+00:00",
"cpu": { "detected": 8, "cgroupLimit": 4 },
"memory": { "availableMb": 5821, "totalMb": 16027 },
"load": { "avg1": 1.42, "avg5": 1.10, "avg15": 0.87 }
},
"warnings": [],
"deprecations": [],
"jobs": [
{
"name": "phpstan_src",
"type": "phpstan",
"success": false,
"time": "2.34s",
"duration": 2.34,
"startedAt": "2026-06-02T09:14:07.530+00:00",
"endedAt": "2026-06-02T09:14:09.871+00:00",
"exitCode": 1,
"output": "src/Foo.php:12 Access to undefined property $bar",
"fixApplied": false,
"command": "vendor/bin/phpstan analyse -c qa/phpstan.neon --no-progress src",
"paths": ["src"],
"skipped": false,
"skipReason": null,
"threshold": {
"warnAfter": 120,
"failAfter": 180,
"warned": false,
"failed": false,
"reason": null
},
"memoryReserved": null,
"memoryPeak": 3500,
"memoryThreshold": null,
"killedReason": null
}
]
}
Top-level fields¶
| Field | Type | Description |
|---|---|---|
version |
integer | Schema version — currently 2. Bumped on breaking changes. |
flow |
string | Flow name (or job name when called from githooks job). |
success |
boolean | true if all non-skipped jobs passed AND no flow-level fail-after / fail-above was crossed. |
totalTime |
string | Human-readable wall-clock time. "0ms" under --dry-run. |
executionMode |
string | "full", "fast", "fast-branch", "fast-dirty" or "files". Reflects the actual mode used. |
passed / failed / skipped |
integer | Counters matching the entries in jobs[]. |
timeBudget |
object or null | Flow-level time budget state. null when not configured. See Time budget block. |
memoryBudget |
object or null | Flow-level memory budget state. null when not configured. See Memory budget block. |
stats |
object or null | RSS sampling + cores attribution. null when stats: false (default). See Stats block. |
runtime |
object or null | Runner snapshot + flow span (version, platform, CI, CPU/cgroup, memory, load, startedAt/endedAt). Always present in flow / flows / job runs; null only on legacy consumers that never attach it. See Runtime diagnostics block. |
inputFiles |
object | Present only in files mode (--files / --files-from). See Input files block. |
flows |
array | Present only in multi-flow runs. List of normal flows actually executed after meta-flow expansion. |
effectiveOptions |
object | Always present in flow / flows / job runs. Each option's value and resolved source. See Effective options and conditions header. |
warnings |
string[] | Always present (empty when no warnings). Validation warnings emitted on stderr during the run. |
deprecations |
object[] | Always present (empty when none). Each entry: {job, oldKey, newKey, removalVersion, kind}. See v3.3 deprecations. |
Per-job fields¶
| Field | Type | Description |
|---|---|---|
name |
string | Job name as configured. |
type |
string | Job type (phpstan, phpcs, custom, …). |
executionOrder |
integer | 1-based position in the execution (completion) order. Always present. Lets consumers reorder client-side regardless of how the text --stats table is sorted (--stats-sort). The jobs[] array itself stays in execution order. |
success |
boolean | true if the job passed AND no per-job fail-after / fail-above was crossed. |
time |
string | Human-readable execution time. |
duration |
float | Execution time in seconds (raw — useful for sorting / comparisons). |
startedAt |
string or null | ISO-8601 timestamp (millisecond precision, e.g. 2026-06-02T09:14:07.530+00:00) when the job began. null for skipped jobs (never executed). |
endedAt |
string or null | ISO-8601 timestamp when the job finished. null for skipped jobs. The pair lets a post-mortem place each job on an absolute timeline — see Runtime diagnostics block. |
exitCode |
integer or null | Underlying tool exit code. null for skipped jobs. |
output |
string | Captured stdout/stderr of the tool, ANSI escapes stripped. |
fixApplied |
boolean | true when the job modified files (fix jobs in non dry-run). |
command |
string | Shell command that was executed (always present; useful under --dry-run). |
paths |
array | Paths analysed (after fast / fast-branch / files filtering). |
skipped |
boolean | true when the job was skipped (fast mode with no matching files, --exclude-jobs, fail-fast cancellation, or fail-above killing the queue). |
skipReason |
string or null | Free-form reason string when skipped: true. |
threshold |
object or null | Per-job time threshold state. null when no warn-after / fail-after configured. See Per-job threshold block. |
memoryReserved |
integer or null | MB reserved by the 2D allocator for this job. null when no short-form memory: was declared. |
memoryPeak |
integer or null | Peak RSS observed (MB). null when the sampler did not run (no stats / Windows). |
memoryThreshold |
object or null | Per-job memory threshold state. null when no memory threshold configured. See Per-job memory threshold block. |
killedReason |
string or null | Set when the job was killed mid-run by a flow-level guard (e.g. "flow memory-budget exceeded"). null otherwise. |
inputFiles |
object | Present on accelerable jobs in files mode. The slice of input files that matched this job's paths. |
Time budget block¶
timeBudget (root) carries the flow-level state under the explicit-null
pattern: present as an object with the same shape always when configured,
null when no time-budget was declared (or --no-time-budget was set):
"timeBudget": {
"warnAfter": 800,
"failAfter": 1200,
"totalJobDuration": 1015.2,
"warned": true,
"failed": false
}
totalJobDuration is the post-hoc sum of executed-job durations in
seconds. warned / failed flip true when their respective threshold
is crossed. A flow with failed: true exits 1 even when every job's
own success is true.
Per-job threshold block¶
threshold (per job) carries per-job warn-after / fail-after state. null
when not configured:
"threshold": {
"warnAfter": 120,
"failAfter": 180,
"warned": false,
"failed": true,
"reason": "execution exceeded fail-after (181.2s)"
}
reason is a string when warned or failed is true, null when both
are false. A job with failed: true flips its top-level success to
false even when the underlying tool exited 0.
Memory budget block¶
memoryBudget (root) carries the flow-level RSS guard. null when not
configured:
"memoryBudget": {
"warnAbove": 3000,
"failAbove": 5000,
"peakObserved": 3743,
"peakAtSecond": 12.34,
"peakAttribution": [{ "name": "phpstan_src", "value": 3500 }],
"warned": true,
"failed": false
}
peakAttribution is the list of jobs in flight at the peak instant with
their individual contribution (MB). When failed: true, the runtime kills
jobs in flight via process->stop(0) and skips queued ones with reason
"flow memory-budget exceeded".
Per-job memory threshold block¶
memoryThreshold (per job): null when not configured. Same shape as
threshold but with warnAbove / failAbove (MB):
"memoryThreshold": {
"warnAbove": 1500,
"failAbove": 2000,
"warned": false,
"failed": true,
"reason": "peak 2150 MB exceeded fail-above (2000 MB)"
}
Stats block¶
stats (root) is emitted only when stats: true (config) or --stats
(CLI). The cores sub-block is always present when active (deterministic
from the schedule); the memory sub-block is present only when the RSS
sampler produced data (Linux/macOS — Windows degrades gracefully):
"stats": {
"cores": {
"limit": 8,
"flowPeak": {
"value": 8,
"atSecond": 0.01,
"jobsInFlight": ["phpstan_src", "phpcs", "phpunit"]
}
},
"memory": {
"flowPeak": {
"value": 3743,
"atSecond": 12.34,
"jobsInFlight": [{ "name": "phpstan_src", "value": 3500 }]
}
}
}
Input files block¶
inputFiles (root) is emitted only in files mode (--files,
--files-from). Per-job inputFiles shows the slice that matched each
accelerable job's paths:
"inputFiles": {
"source": "files-from",
"sourcePath": "/tmp/changed.txt",
"totalProvided": 142,
"totalValid": 138,
"invalid": ["does/not/exist.php"],
"excludedPatterns": ["**/Generated/**"],
"excluded": ["src/Generated/Foo.php"],
"totalAfterExclude": 137
}
source is "files" or "files-from". sourcePath is the manifest path
when --files-from, null for inline --files. excludedPatterns /
excluded / totalAfterExclude are present only when
--exclude-pattern was used. See How-To: --files / --files-from.
Fail-fast and the jobs[] array¶
When --fail-fast cancels the remaining jobs after a failure, the JSON payload still contains every job in the plan. The ones that were not executed appear with:
{
"name": "phpunit_tests",
"type": "phpunit",
"success": true,
"skipped": true,
"skipReason": "skipped by fail-fast",
"exitCode": null,
"time": "0ms"
}
This keeps structured consumers honest: the array size equals the declared plan size, and the skipped counter at the top level reflects both fast-mode skips and fail-fast cancellations.
Runtime diagnostics and absolute timestamps¶
When a CI job hangs for 40 minutes with 39 of them silent, the run log alone cannot tell you whether PHP was blocked, the runner was starved of memory, or the agent's output buffer simply froze. GitHooks answers this with three pieces of pure observability (no behaviour change, no intervention): a runner diagnostics block, absolute startedAt/endedAt timestamps per job and per flow, and a runtime node in JSON v2.
The --diag text block¶
In text mode, --diag prints a snapshot of the runner before the Settings: header:
githooks 3.5.0 · linux · cpus=8 (cgroup limit: 4) · mem=5821 MB / 16027 MB · load=1.42 / 1.10 / 0.87 · 2026-06-02T09:14:07.512+00:00
Settings:
processes = 4 (cli)
…
The block is flushed immediately so that if the run later hangs, the log still carries the snapshot — it is the signal that distinguishes "githooks never started" from "started but blocked".
Emission follows the same factors table as the conditions header:
| Environment | Format | --show-progress |
Diagnostics block |
|---|---|---|---|
| CI (auto-detected) | text | – | stdout, multiline |
| CI | structured / claude-code | on | stderr, multiline |
| CI | structured / claude-code | off | suppressed (clean stdout) |
local + --diag |
text | – | stdout, compact (1 line) |
local + --diag |
structured | on | stderr, compact |
local + --diag |
structured | off | suppressed |
local without --diag |
any | – | not emitted (zero noise) |
In CI the block is automatic (no flag needed) and rendered multiline; locally it is opt-in via --diag and rendered as a single compact line. The channel rule mirrors the header: text → stdout; clean-stdout formats → stderr only with --show-progress, otherwise suppressed so the structured payload stays pristine.
The JSON runtime node is always present
--diag only controls the text block. The runtime JSON node and the per-job startedAt/endedAt fields are part of the JSON v2 contract — they are emitted on every --format=json run regardless of --diag.
Runtime diagnostics block¶
runtime (root, JSON v2) carries the runner snapshot plus the flow span:
"runtime": {
"githooksVersion": "3.5.0",
"platform": "linux",
"ci": "gitlab-ci",
"startedAt": "2026-06-02T09:14:07.512+00:00",
"endedAt": "2026-06-02T09:14:22.694+00:00",
"cpu": { "detected": 8, "cgroupLimit": 4 },
"memory": { "availableMb": 5821, "totalMb": 16027 },
"load": { "avg1": 1.42, "avg5": 1.10, "avg15": 0.87 }
}
| Field | Type | Description |
|---|---|---|
githooksVersion |
string | Resolved package version (unknown if it cannot be determined). |
platform |
string | Short OS token: linux, darwin, windows. |
ci |
string or null | CI name (github-actions, gitlab-ci) or null outside CI. |
startedAt / endedAt |
string | Flow span as ISO-8601 timestamps (millisecond precision). |
cpu.detected |
integer | Effective CPU count used for the thread budget. |
cpu.cgroupLimit |
integer or null | CPU quota from the cgroup (null when none / not on Linux). |
memory.availableMb |
integer or null | System memory available to the runner (MB). null on platforms that cannot report it. |
memory.totalMb |
integer or null | Total system memory (MB). null when unavailable. |
load.avg1 / avg5 / avg15 |
float or null | ⅕/15-minute load averages. null on Windows. |
Platform availability (the contract never breaks — unavailable fields are null):
| Platform | CPU | cgroup limit | Memory | Load |
|---|---|---|---|---|
| Linux | ✅ | ✅ (when set) | ✅ (/proc/meminfo, cgroup) |
✅ |
| macOS | ✅ | – (null) |
best-effort (null if unavailable) |
✅ |
| Windows | ✅ | – (null) |
– (null) |
– (null) |
Memory is the runner's, not PHP's
memory.availableMb / totalMb report system memory (the RAM the runner has), not memory_get_usage() of the PHP process — the post-mortem question is "did the runner run out of RAM", and per-job RSS already lives in memoryPeak and the stats block.
Absolute timestamps¶
Every executed job carries startedAt / endedAt (ISO-8601, millisecond precision) alongside the relative duration. Skipped jobs report null for both (they never ran). Combined with runtime.startedAt / endedAt, this places the whole plan on an absolute wall-clock timeline — so a 40-minute gap in a CI log can be pinned to the exact job (or the silence between jobs) where time was lost.
JUnit¶
JUnit XML compatible with GitHub Actions, GitLab CI, Jenkins and other test reporting tools:
Skipped jobs emit <skipped> elements:
<testcase name="phpstan_src" time="0.000" classname="phpstan">
<skipped message="No staged files match the configured paths"/>
</testcase>
Use with test reporting actions:
# GitHub Actions
- run: vendor/bin/githooks flow qa --format=junit > junit.xml
- uses: mikepenz/action-junit-report@v4
if: always()
with:
report_paths: junit.xml
Code Climate¶
GitLab-compatible Code Quality report. Emits a JSON array where each entry is a CodeIssue:
githooks flow qa --format=codeclimate # prints to stdout
githooks flow qa --format=codeclimate --output=reports/quality.json # writes a file
Each issue's location.path is relative to the current working directory. Absolute paths emitted by tool parsers (phpcs, for instance) are normalised to the workspace root so the report is portable and links correctly in the GitLab UI:
{
"description": "...",
"location": { "path": "src/errors/SyntaxError.php", "lines": { "begin": 3 } }
}
Paths outside the CWD are left untouched.
Integrate directly with GitLab CI — the --output path must match the codequality artifact declared in the job:
qa:
script: vendor/bin/githooks flow qa --format=codeclimate --output=gl-code-quality-report.json
artifacts:
reports:
codequality: gl-code-quality-report.json
SARIF¶
SARIF 2.1.0 report consumable by GitHub Code Scanning, Azure DevOps, and other static-analysis tools:
githooks flow qa --format=sarif # prints to stdout
githooks flow qa --format=sarif --output=reports/qa.sarif # writes a file
artifactLocation.uri is relative to the current working directory, matching the SARIF convention expected by Code Scanning. Absolute paths from tool parsers are normalised; paths outside the CWD are preserved as-is.
Upload to GitHub Code Scanning — the --output path must match the sarif_file argument of the upload step:
- run: vendor/bin/githooks flow qa --format=sarif --output=githooks-results.sarif
- uses: github/codeql-action/upload-sarif@v3
if: always()
with:
sarif_file: githooks-results.sarif
Claude Code stop-hook¶
--format=claude-code emits the Claude Code AI agent stop-hook protocol: empty stdout
and exit 0 on success, a {"decision":"block","reason":…} JSON on failure (also exit 0, by
protocol). It replaces the per-repo bash wrapper that AI integrations used to need.
See the dedicated guide: AI Agent Hooks (Claude Code).
Single job output¶
The --format and --output flags work identically with the job command:
githooks job phpstan_src --format=json # JSON v2 to stdout
githooks job phpcs_src --format=junit # JUnit to stdout
githooks job phpstan_src --format=sarif --output=reports/phpstan.sarif # SARIF to a file
Dry-run¶
Combine --dry-run with any format to see what commands would run:
githooks flow qa --dry-run # text
githooks flow qa --dry-run --format=json # JSON with .command per job
In dry-run the command field per job is the exact shell command that would have executed, so it can be reused by other tools or documented elsewhere.
Effective options and conditions header¶
Every flow, flows and job run prints a conditions header at the start so the operator sees with which processes, fail-fast, mode, budgets, allocator and stats the plan is running, and where each value comes from. One row per option, aligned, every row carries its (source) parenthesis — (default) included — so the column stays aligned and the audit trail is complete:
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
- The header writes to stdout in text mode (default) and to stderr when a structured format is combined with
--show-progress(so stdout payloads stay clean for piping). - The optional
Flows:line appears in declarative, ad-hoc and mixed multi-flow runs (omitted inflow Xandflows Xsingle-flow degenerate). (default)is a meaningful signal — "this fell through, nothing overrode it" — and is the exact answer the operator wants when behaviour is surprising.- The same information is exposed in JSON v2 as the
effectiveOptionsroot block (always present inflow/flows/jobruns, additive to v2):
{
"version": 2,
"flow": "ci-pack",
"flows": ["qa", "lint"],
"effectiveOptions": {
"processes": { "value": 4, "source": "flows.ci-pack.options" },
"failFast": { "value": true, "source": "flows.ci-pack.options" },
"executionMode": { "value": "full", "source": "default" }
}
}
source is one of cli, flows.<X>.options, flows.<alias>.options, flows.options, or default. In ad-hoc and mixed multi-flow runs the per-flow / per-alias options are deliberately ignored, so source is restricted to {cli, flows.options, default}.
The flows[] root array (also additive) lists the normal flows actually executed after meta-flow expansion when relevant. Existing v2 consumers that ignore both fields keep working unchanged.