vis hook
Manage git hooks for your workspace
vis hook
Manage git hooks for your workspace. Supports installing, uninstalling, and migrating from husky or prek (pre-commit framework).
Usage
vis hook <action> [options]Actions
install
Install git hooks by configuring core.hooksPath to point to your hooks directory (.vis/hooks by default):
vis hook installMigrating from
.vis-hooks. The hooks directory moved from.vis-hooksto.vis/hooks.vis hook installperforms the move automatically: an existing.vis-hooksis renamed to.vis/hooks(your stage scripts andconfig.jsoncome along) andcore.hooksPathis re-pointed. Re-runvis hook installonce after upgrading.
uninstall
Remove git hooks and reset core.hooksPath:
vis hook uninstalllist
Print the hooks currently installed in the hooks directory, grouped by stage. Each hook block emitted by the migrator (lines of the form # <id>: <name>) is surfaced with its first command line for quick inspection.
vis hook listvalidate
Sanity-check the hooks directory: verifies core.hooksPath, the dispatcher scripts, each stage script's shell syntax (sh -n), executable permissions, and — if any stage references the bundled prek-runner.mjs — that the runner exists and parses with node --check. Exits non-zero on any error.
vis hook validaterun
Execute a stage's hook script manually, useful for CI or one-off checks. Environment variables forward extra selectors into the bundled runner, so the same hook logic that fires at commit time can be replayed over any file set:
vis hook run pre-commit # run pre-commit against staged files
vis hook run pre-commit --all-files # run against every tracked file
vis hook run pre-commit --from-ref=main --to-ref=HEAD # run against files changed between two refs
vis hook run pre-commit --last-commit # shortcut for --from-ref HEAD~1 --to-ref HEAD--all-files maps to VIS_HOOK_ALL_FILES=1; --from-ref/--to-ref map to VIS_HOOK_FROM_REF/VIS_HOOK_TO_REF. The runner swaps git diff --cached for git ls-files or git diff <from> <to> accordingly. Stage defaults to pre-commit when omitted. --all-files wins if combined with --from-ref/--to-ref or --last-commit.
migrate
Migrate an existing husky or prek (pre-commit framework) setup to vis:
vis hook migrate
vis hook migrate --dry-run # preview: prints what would be written without touching diskThe command auto-detects the source:
- husky — reads
.husky/or.config/husky/, copies shell scripts, uninstalls the husky npm package, and cleanspackage.jsonreferences. - prek / pre-commit — reads
.pre-commit-config.yaml,.yml, orprek.toml, converts eligiblelocalhooks to shell scripts under the hooks directory, backs up the original config as.bak, and attemptsprek uninstallif the binary is on your PATH.
If both a husky directory and a prek config are present, the command errors and asks you to remove one first.
What migrates from prek
Local hooks with language: system, language: script, or language: fail, plus a curated set of remote hooks from pre-commit/pre-commit-hooks, are translated to plain shell + a small bundled Node runner. Everything else still needs the prek binary and is surfaced as a warning or manual step.
| prek feature | migration behavior |
|---|---|
Local system / script / fail hooks | routed through .vis/hooks/.builtins/prek-runner.mjs, which handles staged-file discovery, filters, and chunked argv dispatch |
Remote hooks from pre-commit/pre-commit-hooks — trailing-whitespace, end-of-file-fixer, check-merge-conflict, check-json, mixed-line-ending | translated to bundled builtins (no prek binary required at runtime) |
Other remote repo: https://... entries | skipped with a warning |
Local hooks with toolchain-managed languages (python, node, golang, rust, ruby, docker, pygrep, etc.) | skipped with a warning |
files / exclude (regex filters) | preserved — passed as argv flags to the runner, so user strings can't escape into the outer shell |
types / types_or / exclude_types | preserved for the common types (javascript, typescript, jsx, tsx, json, yaml, markdown, html, css, shell, toml, python); unknown types warn and are skipped |
always_run, pass_filenames: false | forwarded to the runner as flags |
additional_dependencies | merged into package.json devDependencies; pip-style pins (name==x) are surfaced as a manual step instead |
stages (including legacy commit/push/merge-commit aliases) | translated to per-stage hook files |
default_stages | used as the fallback when a hook omits stages |
pass_filenames: true on commit-msg/prepare-commit-msg/pre-rebase/post-* | forwards git's own argument as "$@" (bypasses the runner since pre-commit's filter semantics don't apply) |
Top-level fail_fast: true | prepends set -e to each generated script |
manual stage | skipped silently (not a real git hook) |
minimum_pre_commit_version, ci, default_language_version, default_install_hook_types | ignored |
prek.toml (native format) | parsed via @visulima/fs/toml; YAML takes precedence if both are present |
The bundled runner
When a migration emits a hook that needs staged-file discovery or a built-in, the migrator writes .vis/hooks/.builtins/prek-runner.mjs — a small Node helper that:
- reads staged files from
git diff --cached --name-only --diff-filter=ACM -z(null-separated, safe for names with spaces/newlines) - applies
--files/--excluderegex and--types/--types-or/--exclude-typesfilters in JavaScript (pre-commit semantics) - chunks the file list to a conservative 32 KiB argv budget to stay under ARG_MAX on all platforms
- either dispatches to a named
--builtinor execs the hook's entry with filtered files as trailing argv
All user-supplied values (regex patterns, filenames, hook args) travel via process.argv, never as interpolated shell strings. Commit .vis/hooks/.builtins/ alongside the rest of the hooks directory so teammates get the same behavior.
Options
| Option | Default | Applies to | Description |
|---|---|---|---|
--hooks-dir | .vis/hooks | all | Custom hooks directory |
--dry-run | false | migrate | Preview what would be written without touching disk |
--all-files | false | run | Run against every tracked file (sets VIS_HOOK_ALL_FILES=1) |
--from-ref | — | run | Run against files changed since this ref (sets VIS_HOOK_FROM_REF) |
--to-ref | — | run | Run against files changed up to this ref (sets VIS_HOOK_TO_REF) |
--last-commit | false | run | Shortcut for --from-ref HEAD~1 --to-ref HEAD |
Environment Variables
| Variable | Description |
|---|---|
VIS_GIT_HOOKS=0 | Disable git hooks |
VIS_GIT_HOOKS=1 | Force hooks to run even when skipInCI would skip them under CI (per-job override) |
VIS_GIT_HOOKS=2 | Enable debug output for hooks |
VIS_HOOK_ALL_FILES=1 | Tells the bundled runner to discover files via git ls-files instead of git diff --cached |
VIS_HOOK_FROM_REF=… | Paired with VIS_HOOK_TO_REF, runs the hook against files changed between the two refs |
VIS_HOOK_TO_REF=… | Paired with VIS_HOOK_FROM_REF (above) |
Supported Hooks
vis supports all standard git hooks:
pre-commitpre-merge-commitprepare-commit-msgcommit-msgpost-commitapplypatch-msgpre-applypatchpost-applypatchpre-rebasepost-rewritepost-checkoutpost-mergepre-pushpre-auto-gc
Creating Hook Scripts
After installing, create executable scripts in your hooks directory:
vis hook install
echo '#!/bin/sh
pnpm run lint-staged' > .vis/hooks/pre-commit
chmod +x .vis/hooks/pre-commitCommit the hooks directory to version control so all team members use the same hooks.