Using Git Hooks for Bioinformatics Pipeline Validation

By Dr. Zubair Khalid, DVM, MS, PhD ·

Using Git Hooks for Bioinformatics Pipeline Validation

Formal pipeline validation and day-to-day code review answer different questions. Validation asks whether a workflow performs to specification, demonstrated on a characterized sample panel with defined performance metrics. Code review asks whether today's change is safe to merge. Git hooks for bioinformatics pipeline validation connect the two: they turn the fast, cheap checks that protect a validated pipeline into scripts that Git runs automatically at every commit and push.

By the end of this guide you will be able to build a versioned hook chain that lints pipeline code, rejects floating tool and container versions, validates samplesheets against a schema, runs a smoke test on bundled test data with checksum comparison, blocks oversized files and accidental secrets, and enforces change-control-friendly commit messages. You will also know how to mirror the same checks in GitHub Actions so that bypassed local hooks still get caught, and how to avoid the failure modes that make teams quietly disable hooks.

Version control is baseline practice for computational biology, and git for scientists now extends past source code to pipeline definitions, container pins, environment files, and reference data manifests. Hooks are the part of git that enforces policy on all of it.

What you need on hand:

  • Git 2.9 or newer (the core.hooksPath setting arrived in version 2.9)
  • A pipeline repository: Nextflow, Snakemake, CWL, WDL, or plain scripts
  • Python 3.9 or newer if you want the pre-commit framework and the schema validator shown here
  • ShellCheck installed (apt install shellcheck or brew install shellcheck)
  • A tiny test dataset with expected outputs, a few megabytes at most

Commands are shown for Linux and macOS. Windows users should run hooks under WSL or Git Bash, for reasons covered under Common Mistakes.

Why Git Hooks Belong in Bioinformatics Pipeline Validation

Validation, in the sense used by reference laboratories, means demonstrating that a workflow is fit for purpose with defined metrics. Bogaerts and colleagues validated a whole genome sequencing workflow for Shiga toxin-producing Escherichia coli against 131 isolates from food and human sources characterized with conventional methods, and reported repeatability, reproducibility, accuracy, precision, sensitivity, and specificity above 95 percent for the majority of assays [1]. The same group showed, earlier and for Neisseria meningitidis typing, that classical performance metrics can be adapted to bioinformatics assays such as resistance gene characterization, core genome multilocus sequence typing (cgMLST, a typing scheme based on allele calls across a set of core genes), and serogroup determination, and that laboratories working under a quality system need exactly this evidence to demonstrate fitness for purpose [2]. Clinical settings follow the same logic, from customized solid tumor NGS pipelines [3] to a whole genome pipeline for genetic disorders validated on 45 samples with known variants spanning single nucleotide variants (SNVs), insertions and deletions (indels), copy number variants (CNVs), and structural variants [4]. The tbtAMR pipeline for tuberculosis antimicrobial resistance prediction carried this approach all the way to accreditation under ISO standards [5].

The validated state covers more than code. The meningococcal resistance assay was defined against specific database versions, including ARG-ANNOT, ResFinder, CARD, and NDARO [2]. The STEC workflow supports three alternative gene detection approaches, BLAST+ alignment, KMA kmer-based read mapping, and SRST2 direct read mapping, so a method flip is a change to the validated assay [1]. tbtAMR accepts the WHO resistance catalogue or a user-defined database, so a database swap is a validation event [5]. Change any of these quietly and the published metrics no longer describe the pipeline you are running.

Professional bodies recognize the same fragility. A joint report of the Association for Molecular Pathology, the Association for Pathology Informatics, and the College of American Pathologists recommends in silico approaches for NGS pipeline validation, meaning pure simulated data or existing data files with variants inserted at defined properties. Their rationale maps directly onto hook-friendly test assets: in silico data lets a laboratory test caller behavior, for example analytical sensitivity at low variant allele fraction (the fraction of reads carrying a variant) or across a range of indel sizes, without sequencing additional cases [6].

Git hooks are the enforcement point between that evidence base and everyday development. Formal validation produces the metrics. Hooks guarantee that nothing enters the repository history without passing the cheap checks that protect those metrics: code lints, dependency versions are pinned, samplesheets parse, and the bundled smoke test still produces the expected outputs. Every commit that could affect results passes a gate, and the gate leaves a record in the history that pairs naturally with the change control expectations of a quality system [5].

How Git Hooks Work

Flowchart of the hook chain from staged files through pre-commit, commit-msg and pre-push to the remote, with a bypass branch to CI.

A Git hook is an executable script that Git runs automatically at a fixed point in its lifecycle. Client-side hooks run on your machine during commit, push, merge, or rebase operations. Server-side hooks run on the remote when it receives a push. Hooks live in the hooks directory, .git/hooks by default, where a fresh repository ships only examples ending in .sample. Git activates a hook purely by name and executable bit: a file named pre-commit with run permission fires before every commit.

Three mechanics matter most.

First, exit codes are the interface. If a pre-commit, commit-msg, or pre-push hook exits nonzero, Git aborts the action, and the hook's stderr output is what the user sees. Post hooks (post-commit, post-merge) cannot abort anything because Git ignores their exit codes, which makes them the right place for convenience actions such as rebuilding environments.

Second, hooks receive context. The commit-msg hook gets the path to a file holding the commit message as its first argument. The pre-push hook receives lines of the form local-ref, local-sha, remote-ref, remote-sha on standard input, one per ref being pushed. pre-commit receives no arguments, so it must ask Git what is staged, meaning which changes are recorded in the index, the snapshot Git will turn into the next commit.

Third, hooks are not versioned. Nothing in .git/hooks is copied by clone, fetch, or pull, so a fresh clone starts with no active hooks at all. Since Git 2.9, the configuration key core.hooksPath redirects Git to any directory you choose, which is the standard trick for keeping hooks under version control inside the repository itself:

git config core.hooksPath .githooks

After that command, Git ignores .git/hooks entirely and runs scripts from .githooks instead. The setting lives in the local repository config, so every person who clones the repo must run it once. That single fact causes more silent hook failures than anything else in this article.

The table below summarizes the hooks worth knowing for pipeline work.

HookWhen Git runs itWhat belongs thereCan it block the action?
pre-commitJust before a commit is recordedLinting, schema checks, dependency pinning, secret and file size checks on staged filesYes, nonzero exit aborts the commit
commit-msgAfter you save the commit messageMessage format, change request or ticket referenceYes
pre-pushBefore objects are uploadedSmoke test on bundled data, checksum regressionYes
post-mergeAfter a pull mergesRebuild conda environments, pull containersNo, exit code ignored
pre-rebaseBefore a rebase startsWarn when rebasing away from a validated tagYes
pre-receive (server side)When the remote accepts a pushPolicy enforcement for every userYes, rejects the push

What to Validate at Each Hook

Comparison table of the four validation tiers by where they run, their data, runtime and purpose.

The design principle is a tiered corpus. Fast deterministic checks run constantly, expensive evidence runs rarely, and each tier uses data sized for its runtime budget.

Tier 1, pre-commit, inspects only the staged files. Static checks dominate: lint shell and Python, validate YAML and samplesheet schemas, confirm container images and conda packages are pinned to exact versions, block files over a size threshold, and scan for private keys. None of this executes the pipeline, so it belongs where it runs dozens of times a day.

Tier 2, pre-push, runs the pipeline itself on bundled test data and compares outputs against recorded checksums. A smoke test is a minimal end-to-end run whose only job is to prove the pipeline still executes and still produces recognizable results. The data should be small and known by construction. The AMP, Pathology Informatics, and CAP recommendations describe exactly this kind of in silico asset for clinical laboratories [6], and for metagenomics, MeStanG generates Nanopore datasets with a specified number of reads per organism from reference sequences and error profiles, so the expected sample composition is known in advance [7].

Tier 3, continuous integration on a pull request, runs reference materials and comparative benchmarks. ClinBioNGS, a fully containerized Nextflow pipeline for somatic NGS panels, was validated against SEQC2 reference datasets across six commercial panels and benchmarked on 2,024 clinical tumor samples [8]. IS-Seq validated its integration site abundance estimates against INSPIIRED, the most widely used published workflow in that field [9]. These runs take tens of minutes to hours and belong on CI infrastructure, not on a laptop.

Tier 4, scheduled or release-gated runs, re-executes the full validation corpus and recomputes the published metric set: the 131-isolate STEC panel [1], the 45 known-variant samples of the genetic disorder pipeline [4], or the three control datasets containing four laboratory strains at defined mixing ratios used to validate PHARE for Plasmodium falciparum compositional profiling [10]. A hook never runs tier 4. A hook's job is to make sure that when tier 4 runs, the pipeline it measures is the pipeline you think it is.

TierWhere it runsDataTypical runtimePurpose
1pre-commitStaged files onlySecondsStatic policy: lint, pinning, schema, secrets, size
2pre-pushBundled in silico or synthetic test files2 to 10 minutesBehavioral regression against known outputs
3CI on pull requestReference materials, benchmark corporaTens of minutesComparison against reference implementations
4Scheduled or release gateFull characterized validation corpusHours to daysRecomputes the published performance metrics

Step-by-Step: Building a Validation Hook Chain

The walkthrough uses a small Nextflow-style repository, but every step works identically for Snakemake, CWL, WDL, or plain scripts. Run the commands at the repository root.

Step 1: Inspect the hooks directory

ls .git/hooks
head -20 .git/hooks/pre-commit.sample

Expected output:

applypatch-msg.sample     post-update.sample        pre-rebase.sample
commit-msg.sample         pre-applypatch.sample     pre-receive.sample
fsmonitor-watchman.sample pre-commit.sample         prepare-commit-msg.sample
post-checkout.sample      pre-merge-commit.sample   push-to-checkout.sample
post-merge.sample         pre-push.sample           update.sample

Every file ends in .sample, which means no hook is active. Git ships these as documentation, and pre-commit.sample is a runnable skeleton that shows the basic pattern of scanning staged content for leftover merge conflict markers.

Step 2: Create a versioned hooks directory and point Git at it

mkdir -p .githooks
git config core.hooksPath .githooks
git config core.hooksPath

Expected output:

.githooks

The core.hooksPath value can be absolute or relative to the top of the working tree. From now on, Git looks for hooks in .githooks and ignores .git/hooks. Add this command to your README or setup script, because a fresh clone does not inherit it.

Step 3: Write the pre-commit hook

Create .githooks/pre-commit:

#!/usr/bin/env bash
# Fast static checks on staged files. Budget: under 30 seconds.
set -euo pipefail

fail=0

# Everything staged that is Added, Copied, Modified, or Renamed.
# Excluding Deleted matters, otherwise linting a removed path crashes the loop.
staged=$(git diff --cached --name-only --diff-filter=ACMR)

# 1. Lint staged shell scripts
while IFS= read -r path
do
  if [[ "$path" == *.sh && -f "$path" ]]
  then
    if ! shellcheck "$path"
    then
      fail=1
    fi
  fi
done <<< "$staged"

# 2. Reject floating container or conda versions in staged workflow files
if grep -qE '(containers?\.ya?ml|nextflow\.config|envs/.*\.ya?ml)$' <<< "$staged"
then
  if git diff --cached | grep -E '^\+' | grep -E '(conda:|docker:|singularity:|container:)' | grep -E '(:latest|:\*|\$\{)'
  then
    echo "ERROR: staged change adds an unpinned container or conda dependency" >&2
    echo "       pin an exact version tag or digest, then restage and commit" >&2
    fail=1
  fi
fi

# 3. Block accidental large files, the classic FASTQ-in-git mistake
while IFS= read -r path
do
  if [[ -f "$path" ]]
  then
    size=$(stat -c%s "$path" 2>/dev/null || stat -f%z "$path")
    if [[ "$size" -gt 10000000 ]]
    then
      echo "ERROR: $path is larger than 10 MB, store it in object storage or LFS" >&2
      fail=1
    fi
  fi
done <<< "$staged"

if [[ "$fail" -ne 0 ]]
then
  echo "pre-commit: checks failed, commit aborted" >&2
fi

exit "$fail"

Parameters that matter here:

  • set -euo pipefail makes the script exit on the first failing command (-e), treats unset variables as errors (-u), and treats a failure anywhere in a pipeline as a pipeline failure (-o pipefail). Without pipefail, a grep that finds nothing mid-pipeline would be masked by a later command succeeding.
  • -diff-filter=ACMR selects Added, Copied, Modified, and Renamed paths and drops Deleted ones, which prevents the lint loop from touching paths that no longer exist.
  • The 10,000,000 byte (10 MB) size threshold is a team policy choice, not a Git rule. Tune it to your repository.
  • Pinning to an exact tag or a digest (a content-addressed sha256: identifier of the image) is what keeps a containerized workflow identical between the validated run and today's run.

Make it executable and check the syntax:

chmod +x .githooks/pre-commit
bash -n .githooks/pre-commit

bash -n parses the script without running it and prints nothing when the syntax is clean. Silence here is success.

Step 4: Write the commit-msg hook

Change control starts with messages a human can audit months later. This hook enforces a conventional format and reserves a type for reference data changes:

#!/usr/bin/env bash
# .githooks/commit-msg: enforce a change-control message format
msg_file="$1"

if ! grep -qE '^(feat|fix|data|docs|refactor|test|chore)(\([a-z0-9._-]+\))?!?: .{10,}' "$msg_file"
then
  echo "commit-msg: message rejected" >&2
  echo "  expected format: type(scope): description of at least 10 characters" >&2
  echo "  allowed types: feat, fix, data, docs, refactor, test, chore" >&2
  echo "  use data(...) for reference or database updates, those require revalidation" >&2
  exit 1
fi

The hook's single argument, $1, is the path to a temporary file containing the message. The data(...) type is a lab convention worth adopting: when the change is a database or reference update, the commit itself announces that the validated state may have shifted.

chmod +x .githooks/commit-msg

Step 5: Write the pre-push smoke test

This is the hook that actually runs the pipeline. Bundle a tiny test dataset and record output checksums (a checksum is a fixed-length fingerprint of a file, and sha256 is the standard choice):

mkdir -p test_data/expected
# generate checksums once, from a known-good run
cd test_data/expected
sha256sum variants.vcf.gz multiqc_report.html > checksums.sha256
cd ../..
cat test_data/expected/checksums.sha256

Expected output (your values will differ):

9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08  variants.vcf.gz
2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae  multiqc_report.html

Then create .githooks/pre-push:

#!/usr/bin/env bash
# .githooks/pre-push: run the pipeline on bundled test data before every push
set -euo pipefail

echo "pre-push: running smoke test on bundled test data"

nextflow run . -profile test -resume -ansi-log false

(cd test_data/expected && sha256sum -c checksums.sha256)

echo "pre-push: smoke test passed"

Parameters that matter:

  • -profile test selects the test configuration, the standard nf-core convention that points the pipeline at bundled minimal data instead of real runs.
  • -resume caches process outputs so repeated pushes take seconds instead of minutes.
  • -ansi-log false disables the animated terminal output, which keeps the log readable when the hook output is captured to a file.
  • sha256sum -c reads the checksum file and verifies each entry, exiting nonzero on any mismatch.

A successful push now looks like this:

pre-push: running smoke test on bundled test data
N E X T F L O W  ~  version 24.04.0
Launching `./main.nf` [soggy_curie] DSL2 - revision: 1a2b3c4d5e
executor >  local (3)
[b3/8a1e2c] process > VALIDATE_SAMPLESHEET (1) [100%]
[d1/9f0a11] process > RUN_FASTQC           (1) [100%]
[e5/2b7d44] process > COLLECT_REPORTS      (1) [100%]
Pipeline executed successfully
test_data/expected/variants.vcf.gz: OK
test_data/expected/multiqc_report.html: OK
pre-push: smoke test passed

The launch name (soggy_curie) and the version line vary with your Nextflow install. The rest is the shape to expect. If a checksum fails, the hook prints the real sha256sum warning and the push dies:

test_data/expected/variants.vcf.gz: FAILED
sha256sum: WARNING: 1 computed checksum did NOT match
error: failed to push some refs to 'github.com/your-lab/pipeline.git'

That warning is the entire point of the hook. Something changed an output, and the change cannot reach the remote until someone explains it.

An optional post-merge hook keeps environments honest after pulls:

#!/usr/bin/env bash
# .githooks/post-merge: refresh the conda environment after pulling changes
conda env update --file environment.yml --prune

Step 6: Trigger a deliberate failure and confirm the gate holds

Verification matters as much as construction. Stage a violation on purpose:

echo "  - fastqc:latest" >> containers.yaml
git add containers.yaml
git commit -m "feat: add fastqc container"

Expected output:

ERROR: staged change adds an unpinned container or conda dependency
       pin an exact version tag or digest, then restage and commit
pre-commit: checks failed, commit aborted

Confirm the commit did not happen and the change is still staged:

git status
On branch main
Changes to be committed:
  modified:   containers.yaml

Fix the pin to an exact version, restage, and the commit goes through. This five-minute exercise is also how you re-verify hooks after any Git upgrade or machine change.

Step 7: Adopt the pre-commit framework for the file-level checks

Hand-rolled hooks are transparent, but the pre-commit framework adds a curated hook library, per-file filtering, and automatic management of the hook entry point:

python -m pip install pre-commit
pre-commit install

Expected output:

pre-commit installed at .githooks/pre-commit

The framework respects core.hooksPath, so with the setting from Step 2 it installed its runner into .githooks, which would overwrite your hand-written pre-commit. The clean pattern is to let the framework own pre-commit and keep commit-msg and pre-push as raw scripts, which coexist without conflict. Configure the framework in .pre-commit-config.yaml:

repos:
  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v4.6.0
    hooks:
      - id: check-yaml
      - id: end-of-file-fixer
      - id: trailing-whitespace
      - id: detect-private-key
      - id: check-added-large-files
  - repo: local
    hooks:
      - id: samplesheet-schema
        name: samplesheet passes schema validation
        entry: python scripts/validate_samplesheet.py
        language: system
        files: 'samplesheet.*\.(csv|tsv|yaml|yml)$'

Config keys that matter:

  • rev pins the version of the remote hook repository, the same pinning discipline you apply to containers. Run pre-commit autoupdate periodically to raise it deliberately.
  • files is a regular expression selecting which staged paths trigger the hook.
  • language: system runs entry with whatever is on PATH, which is fast but couples the check to the local environment. Use it for scripts you control.
  • check-added-large-files blocks new files over its default threshold of 500 kB, and accepts an argument to raise the threshold if your bundled test data is larger.

A samplesheet is the table mapping sample identifiers to input files, and it is the most common hand-edited file in any sequencing pipeline. A validator at scripts/validate_samplesheet.py can be as simple as required columns plus extension checks:

#!/usr/bin/env python3
"""Exit 0 if every samplesheet argument has the required columns and FASTQ paths."""
import csv
import sys
from pathlib import Path

REQUIRED = {"sample", "fastq_1", "fastq_2"}
FASTQ_SUFFIXES = (".fastq.gz", ".fq.gz", ".fastq", ".fq")

def main(paths):
    problems = []
    for raw in paths:
        sheet = Path(raw)
        delimiter = "\t" if sheet.suffix == ".tsv" else ","
        with sheet.open(newline="", encoding="utf-8") as handle:
            rows = list(csv.DictReader(handle, delimiter=delimiter))
        if not rows:
            problems.append(f"{sheet}: no data rows")
            continue
        missing = REQUIRED - rows.keys()
        if missing:
            problems.append(f"{sheet}: missing columns {sorted(missing)}")
        for number, row in enumerate(rows, start=2):
            for column in ("fastq_1", "fastq_2"):
                value = (row.get(column) or "").strip()
                if value and not value.lower().endswith(FASTQ_SUFFIXES):
                    problems.append(f"{sheet} line {number}: {column}={value} is not a FASTQ path")
    for problem in problems:
        print(f"samplesheet-schema: {problem}", file=sys.stderr)
    return 1 if problems else 0

if __name__ == "__main__":
    sys.exit(main(sys.argv[1:]))

Run everything across the whole repository to prove the configuration:

pre-commit run --all-files

Expected output:

check-yaml............................................................Passed
end-of-file-fixer.....................................................Passed
trailing-whitespace...................................................Passed
detect-private-key....................................................Passed
check-added-large-files...............................................Passed
samplesheet-schema....................................................Passed

Running the Same Checks on GitHub

Client hooks only ever run on machines where someone cloned the repository and ran the setup. GitHub for researchers adds the server-side layer that makes the policy real: protected branches, required status checks, and GitHub Actions.

The parity principle is simple: every check a hook can run, CI must also run, using the same commands and the same test data. A minimal workflow:

# .github/workflows/pipeline-tests.yml
name: pipeline-tests
on: [pull_request]
jobs:
  checks:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-java@v4
        with:
          distribution: temurin
          java-version: "17"
      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"
      - run: pip install pre-commit shellcheck-py
      - run: pre-commit run --all-files
      - run: curl -s https://get.nextflow.io | bash
      - run: ./nextflow run . -profile test

Mark this workflow a required status check for your main branch in the repository settings under branch rules. After that, a commit that arrived with -no-verify still cannot be merged until the same checks pass on a clean machine. That closes the biggest hole in client-side enforcement.

On self-managed infrastructure, the equivalent control is a pre-receive hook, which GitHub Enterprise Server supports and which GitLab exposes as server hooks. On github.com itself, custom server hooks are not available to regular repositories, so required status checks are the mechanism to use.

One more GitHub-side habit pairs well with hooks: tag the validated state. When a validation run completes against a commit, cut an annotated tag:

git tag -a v1.3.0-validated -m "Validation corpus passed, see VALIDATION.md"
git push origin v1.3.0-validated

The tag plus the commit history is the change-control record that quality-system audits trace, mapping each pipeline version to the evidence that supports it [5].

Common Mistakes and Limitations

The hook is not executable, so it never runs. Git skips a hook silently when it lacks the executable bit, though recent Git versions print a hint: hint: The '.githooks/pre-commit' hook was ignored because it's not set as executable. Fix with chmod +x and verify with ls -l .githooks that the rwx bits are present.

Windows line endings break the shebang. A hook edited on Windows and saved with CRLF endings fails with bash: .githooks/pre-commit: /bin/bash^M: bad interpreter: No such file or directory. The ^M is the carriage return. Fix the file with dos2unix and prevent recurrence with a .gitattributes line: *.sh text eol=lf.

A fresh clone has no hooks and no warning. core.hooksPath lives in local config, which clone does not copy, so teammates get a repository that looks fully set up and silently runs zero checks. Put git config core.hooksPath .githooks and pre-commit install in the setup documentation or a bootstrap script, and have CI fail when the hooks directory is missing or non-executable.

The hook checks the working tree, not the commit. A raw pre-commit hook that lints files on disk is checking content that can differ from what is staged: edit a file, stage only half of it, and the hook sees the whole file while the commit records half. For strict correctness, inspect staged content with git diff, cached, or use the pre-commit framework, which handles staged content for you.

Hooks get bypassed. git commit, no-verify and git push, no-verify skip client hooks entirely, and edits made through the GitHub web interface never trigger them. Client hooks are a convenience layer. The enforceable layer is server-side: protected branches with required status checks, or pre-receive hooks on platforms that support them.

Slow hooks get disabled. If pre-commit takes five minutes, someone on the team will bypass it or delete it. Keep pre-commit under roughly 30 seconds, put the pipeline smoke test in pre-push where it runs once per push, and keep multi-hour corpora in CI.

Deleted and renamed files crash naive loops. git diff, cached, name-only includes deleted paths, and running a linter on a path that no longer exists fails with No such file or directory. Filter with -diff-filter=ACMR or test for existence before invoking a tool.

Hooks pass locally and fail in CI. A hook that depends on whatever conda environment happens to be active is checking a different pipeline than CI is checking. Run both through the same pinned environment or container definition, which is also the reproducibility argument for fully containerized workflows such as ClinBioNGS [8].

And the honest limitation. Hooks verify the checks you thought to write, on the data you thought to bundle. They cannot detect a performance regression that only appears on real biological diversity, which is why the literature validates on characterized corpora of real isolates [1] and known-variant samples [4] rather than on bundled test files. Hook-based checks complement that evidence, they never substitute for it, and any clinical or diagnostic deployment needs formal validation by the laboratory that runs it.

Practical Tips That Keep Hooks Alive

  • Budget by lifecycle point. Seconds for pre-commit, minutes for pre-push, hours for CI, days for the validation corpus. If a check does not fit its budget, move it up a tier.
  • Make smoke test data synthetic or in silico. Test files built from simulated reads or inserted variants [6] and synthetic communities with known organism composition [7] are small, deterministic, and free of patient privacy constraints.
  • Version the reference data, not just the code. Record database names, versions, and checksums in a tracked file, and use the data(...) commit type so reference updates announce themselves and can trigger revalidation.
  • Tag validated releases and keep a VALIDATION.md that maps each tag to its corpus and metric summary. Quality-system accreditation, such as the ISO accreditation achieved by the tuberculosis AMR pipeline [5], depends on tracing any reported result back to a pipeline version and its validation evidence.
  • Log hook failures. Append pre-push failures to a file or CI artifact so recurring flakiness is visible instead of anecdotal.
  • Run pre-commit run, all-files before opening a pull request. It catches the trailing whitespace and end-of-file drift that makes review diffs noisy.
  • Keep hooks and CI in lockstep. One source of truth for the check commands, a script in the repository called by both the hook and the workflow, prevents the works-on-my-machine gap.

Sensible next steps, in order: add the post-merge environment refresh, extend the smoke test corpus with a few in silico files at harder parameter settings, wire the tier 3 CI job, and schedule the first tier 4 revalidation run against your tagged release.

Frequently Asked Questions

Do Git hooks replace formal bioinformatics pipeline validation?

No. Hooks run fast checks at commit and push time to keep a validated pipeline from drifting, while formal validation demonstrates performance on a characterized sample corpus with defined metrics. Treat hooks as a guardrail, not as the validation evidence itself.

What is the difference between a pre-commit hook and CI?

A pre-commit hook runs on the committer's machine in a few seconds and blocks the commit locally, while CI runs after the push on a clean server. Use hooks for instant feedback and CI as the authoritative, non-bypassable gate.

How do I share hooks with everyone in my lab?

Store them in a versioned directory such as .githooks and have every user run git config core.hooksPath .githooks once, or use the pre-commit framework and document pip install pre-commit plus pre-commit install in your setup instructions.

Can a developer bypass the hooks?

Yes, git commit, no-verify and git push, no-verify skip client hooks, which is why protected branches with required status checks should re-run the same checks on the server.

Do Git hooks work on Windows?

Yes, but hook scripts need executable permissions and Unix line endings, and the shebang must point to an interpreter that exists, so most Windows teams run hooks under WSL or Git Bash and add a .gitattributes rule forcing LF endings for scripts.

How big should the test data in a pre-push hook be?

Keep it small enough to finish in a few minutes, typically a handful of tiny synthetic or in silico modified files with known expected outputs, and save the full validation corpus for scheduled CI runs.

Do hooks run for edits made directly on GitHub.com?

No, client-side hooks only run on a machine with the repository checked out, so web edits skip them entirely, and protected branches with required status checks are the only reliable control for that path.

Which hook should run the pipeline smoke test?

The pre-push hook is the usual choice because it runs the few minutes before code leaves your machine, while teams that prefer faster commits move the smoke test to a pull-request CI job instead.

Sources

  1. Validation strategy of a bioinformatics whole genome sequencing workflow for Shiga toxin-producing Escherichia coli using a reference collection extensively characterized with conventional methods
  2. Validation of a Bioinformatics Workflow for Routine Analysis of Whole-Genome Sequencing Data and Related Challenges for Pathogen Typing in a European National Reference Center: Neisseria meningitidis as a Proof-of-Concept
  3. Validation of a Customized Bioinformatics Pipeline for a Clinical Next-Generation Sequencing Test Targeting Solid Tumor-Associated Variants.
  4. Test development, optimization and validation of a WGS pipeline for genetic disorders
  5. Bringing tuberculosis genomics to the clinic: development and validation of a comprehensive pipeline to predict antimicrobial susceptibility from genomic data, accredited to ISO standards.
  6. Recommendations for the Use of In silico Approaches for Next Generation Sequencing Bioinformatic Pipeline Validation: A Joint Report of the Association for Molecular Pathology, Association for Pathology Informatics, and College of American Pathologists.
  7. MeStanG-Resource for High-Throughput Sequencing Standard Data Sets Generation for Bioinformatic Methods Evaluation and Validation
  8. ClinBioNGS: A Clinical Bioinformatics Pipeline for Integrated Analysis of Somatic Next-Generation Sequencing Cancer Panels.
  9. IS-Seq: a bioinformatics pipeline for integration sites analysis with comprehensive abundance quantification methods
  10. PHARE: a bioinformatics pipeline for compositional profiling of multiclonal Plasmodium falciparum infections from long-read Nanopore sequencing data