Development takes place on Tangled, however, I do maintain a GitHub mirror. Please make issues and PRs to the Tangled repository.
nixhooks #
A small, fast, drop-in replacement for git-hooks.nix.
No flake required, no flake-parts, no systems, no Python, no slop, just
plain ol' Nix and Bash.
Usage (non-flake) #
{ pkgs ? import <nixpkgs> { } }:
let
nixhooks = import (fetchTarball "https://next.tangled.org/xrpc/sh.tangled.repo.archive?repo=at%3A%2F%2Fdid%3Aplc%3Ayaryxzbwui6ejiegzysy6ipp%2Fsh.tangled.repo%2Fnixhooks&ref=main&format=tar.gz") { inherit pkgs; };
in
nixhooks.mkHooks {
hooks = {
shellcheck = {
entry = "${pkgs.shellcheck}/bin/shellcheck";
files = "\\.sh$";
stages = [ "pre-commit" "pre-push" ];
};
nixfmt-check = {
entry = "${pkgs.nixfmt}/bin/nixfmt";
args = [ "--check" ];
files = "\\.nix$";
};
};
}
$ nix-build -A packages.install-hooks && ./result/bin/install-hooks
Or, install hooks automatically on shell entry with a shell.nix:
{ pkgs ? import <nixpkgs> { } }:
let
hooks = import ./default.nix { inherit pkgs; };
in
hooks.wrapShell (pkgs.mkShell {
packages = [ pkgs.jq ];
})
wrapShell keeps the shell's inputs and appends to its existing shellHook.
If you'd rather splice it in yourself, hooks.shellHook is the bare snippet.
Usage (flake) #
nixhooks.lib.<system> is mkHooks/presets built from nixhooks' own
pinned nixpkgs for that system.
{
inputs.nixhooks.url = "git+https://tangled.org/poacher.dev/nixhooks";
outputs = { self, nixpkgs, nixhooks }: let
pkgs = nixpkgs.legacyPackages.x86_64-linux;
hooks = nixhooks.lib.x86_64-linux.mkHooks {
hooks = {
shellcheck = {
entry = "${pkgs.shellcheck}/bin/shellcheck";
files = "\\.sh$";
};
};
};
in {
packages.x86_64-linux = hooks.packages;
apps.x86_64-linux = hooks.apps;
devShells.x86_64-linux.default = hooks.wrapShell (pkgs.mkShell {
packages = [ pkgs.jq ];
});
};
}
$ nix run .#install-hooks
lib.withHooks #
withHooks is sugar over mkHooks for flakes. Pass it your hook config
alongside your regular flake outputs and it merges the generated
apps/packages and wires install-hooks into devShells.<system>.default
Hooks are passed as a hooks argument keyed by system, kept separate from
your regular flake outputs. Each system's attrset may also carry its own
settings.
{
inputs.nixhooks.url = "git+https://tangled.org/poacher.dev/nixhooks";
outputs = { self, nixpkgs, nixhooks }: let
pkgs = nixpkgs.legacyPackages.x86_64-linux;
in nixhooks.lib.withHooks {
hooks.x86_64-linux = {
inherit (nixhooks.lib.x86_64-linux.presets) alejandra statix;
settings = {
parallel = true;
tangled = {
enable = true;
attr = "run-hooks";
flake = true;
};
};
};
packages.x86_64-linux = { /* ... */ };
apps.x86_64-linux = { /* ... */ };
devShells.x86_64-linux.default = pkgs.mkShell {
packages = [ pkgs.jq ];
};
};
}
Migrating #
- The top-level
nixhooks = { hooks = ...; settings = ...; }argument towithHookshas been removed. Usehooks.<system>instead, with settings athooks.<system>.settings. mkHooksno longer exposes its derivations at the top level; they live underpackages(e.g.hooks.install-hooksis nowhooks.packages.install-hooks). For ashellHook, preferhooks.shellHookorhooks.wrapShell. Non-flake CI pipelines generated before this change runnix-build -A run-hooks, so regenerate them.
Hook spec #
hooks.<name> = {
enable = true; # default true; set false to disable without deleting the entry
entry = "/nix/store/.../bin/tool"; # the command to run, exclusive with script
args = [ ]; # extra args passed before matched filenames
files = ".*"; # ERE regex (bash [[ =~ ]]) tested against each candidate path
exclude = "^$"; # ERE regex; "^$" (default) excludes nothing
stages = [ "pre-commit" ]; # subset of [ "pre-commit" "pre-push" "commit-msg" ]
pass_filenames = true; # append matched files as trailing args
always_run = false; # run once with no file filtering/passing
path = [ ]; # packages whose bin/ dirs are prepended to PATH for entry
serial = true; # if false this hook will be run in parallel; see "Parallel execution" below
required = true; # if false a failing hook only warns instead of blocking the git action
fallback = "tool"; # defaults to entry's filename, looked up on PATH when entry isn't found
script = null; # bash body run in place of entry; see below
};
<name> must match [A-Za-z0-9_.-]+
A hook fails if entry exits nonzero, or if neither entry nor fallback
can be found. A failing required hook blocks the commit/push; any other
hook just prints a warning.
path is for tools that internally dispatch to a sibling binary via PATH
(e.g. cargo finding cargo-clippy) -- entry itself is always invoked by
absolute path regardless of path.
script is for hooks that need a bit of shell around their tool. It's
embedded in the generated hook script and run like a standalone bash script
with args and the matched files as "$@". You may find tools with
nixhooks_tool, which prints the store binary if it exists and otherwise looks
the name up on PATH:
gofmt = {
script = ''
gofmt="$(nixhooks_tool ${pkgs.go}/bin/gofmt gofmt)" || exit
unformatted="$("$gofmt" -l "$@")" || exit
[ -z "$unformatted" ] || { echo "$unformatted"; exit 1; }
'';
files = "\\.go$";
};
A commit-msg-staged hook receives the path to the temporary file
containing the commit message as its sole file. files/exclude/
pass_filenames apply to that single path the same way they apply to real
filenames in pre-commit/pre-push.
Presets #
presets is a small built-in catalog of common tool configs. These are
functors which accept either no arguments, or allow for their options to be
overriden in an attrSet.
nixhooks.mkHooks {
hooks = {
shellcheck = nixhooksLib.presets.shellcheck;
alejandra = nixhooksLib.presets.alejandra { stages = [ "pre-push" ]; };
};
}
See lib/presets.nix for the full list of presets.
Parallel execution #
By default every hook runs sequentially. Passing settings.parallel = true to
mkHooks enables a two-phase model where every hook not marked serial runs
concurrently, then the remaining hooks run sequentially. Note that parallelism
can actually decrease your hook execution speed if its already fast. Only use
it when needed.
nixhooks.mkHooks {
hooks = {inherit (nixhooksLib.presets) shellcheck alejandra statix;};
settings.parallel = true;
}
Output from the concurrent batch is buffered per hook and flushed once the entire batch finishes in the declared order.
Only mark a hook serial = false if it doesn't mutate the files it's
matched against, or if it's provably safe to race against every other
serial = false hook in the same stage.
Non-Nix contributors #
The hooks install-hooks sets up live in the Nix store, so contributors
without Nix can't use them. Enabling settings.portable generates a copy
you commit to the repo instead:
nixhooks.mkHooks {
hooks = { /* ... */ };
settings.portable = {
enable = true;
dir = ".nixhooks"; # relative to the repo root
};
}
$ nix run .#gen-portable-hooks
gen-portable-hooks: wrote .nixhooks/commit-msg
gen-portable-hooks: wrote .nixhooks/pre-commit
gen-portable-hooks: wrote .nixhooks/pre-push
Contributors without Nix then enable them with:
$ git config core.hooksPath .nixhooks
CI/CD #
nixhooks can also autogenerate CI workflows based on your selected hooks.
Hooks staged only for commit-msg (e.g. the commitlint preset) are
excluded from the generated CI pipeline's run-hooks invocation as CI has
no commit message to check. A hook declaring multiple stages including
commit-msg still runs in CI for its other stage(s).
Tangled pipelines #
nixhooks.mkHooks {
hooks = { /* ... */ };
settings.tangled = {
enable = true;
attr = "run-hooks";
flake = true;
engine = "microvm";
when = [
{
event = ["push" "pull_request"];
branch = ["main"];
}
];
};
}
$ nix-build -A packages.gen-tangled-pipeline && ./result/bin/gen-tangled-pipeline
gen-tangled-pipeline: wrote .tangled/workflows/hooks.yml
This writes a .tangled/workflows/hooks.yml in the same manner as
install-hooks which you commit to the repo. Note that this isn't wired into
shellHook like install-hooks, so you'll need to trigger it manually.
GitHub Actions #
nixhooks.mkHooks {
hooks = { /* ... */ };
settings.githubActions = {
enable = true;
attr = "run-hooks";
flake = true;
runsOn = "ubuntu-latest";
cache = true; # nix-community/cache-nix-action
on = {
push = { branches = [ "main" ]; };
pull_request = { branches = [ "main" ]; };
};
};
}
$ nix-build -A packages.gen-github-actions-workflow && ./result/bin/gen-github-actions-workflow
gen-github-actions-workflow: wrote .github/workflows/hooks.yml
Note that I don't actively used GitHub. If you find any bugs then please open an issue.