Skip to content

Quality hooks

ward init --quality-hooks installs local hooks for the phase profiles generated by init:

Terminal window
ward init --integration none --gate format,tests --quality-hooks
  • .git/hooks/pre-commit runs ward check --profile pre-commit --format json after the shebang and before existing hook logic. It passes each staged file as --path (added, copied, modified, or renamed; deleted files are left out), so gates with a partial table check only the commit’s files, and gitleaks scans the staged files instead of the history. When nothing qualifies, as for a commit that only deletes files, every gate runs in full. A file staged in part is checked as it is in the working tree.
  • .git/hooks/pre-push runs ward check --profile pre-push --format json after the shebang and before existing hook logic.
  • Compilation is intentionally part of pre-push (and ci), not pre-commit; the fast commit hook avoids paying for a compile on every edit.
  • Hooks report results as JSON on stdout so a coding agent that commits or pushes can read which findings to fix.
  • A failed check returns non-zero and blocks the commit or push.

Ward uses marker-bounded sections. Existing user hook content and executable permissions are preserved. Re-running --quality-hooks refreshes both managed sections without adding duplicates. To remove only Ward’s quality sections, run:

Terminal window
ward init --remove-quality-hooks

This does not remove unrelated user content, other Ward-managed integrations, or the sections of other projects. If a hook has an incomplete or duplicate Ward quality marker, initialization reports a conflict and leaves the hook unchanged; repair the markers manually before retrying.

Ward asks Git which hooks directory it runs (git rev-parse --git-path hooks), so a configured core.hooksPath and the shared hooks of a linked worktree are honoured. When Git cannot answer, for example outside a Git work tree, Ward uses .git/hooks in the project.

A hook manager that regenerates its hook files, such as husky or lefthook, may drop Ward’s section when it rewrites them. Add ward check --profile pre-commit to that manager’s configuration instead.

When the project sits below the repository root, for example apps/api in a monorepo, Ward adds its sections to the repository’s hooks and never creates .git inside the project. Each section is keyed by the project path, so several Ward projects can share one hook, and refreshing or removing one project’s section leaves the others alone:

Terminal window
# ward-owned: quality-hooks v1 begin apps/api
if ! git diff --cached --quiet -- 'apps/api'; then
if ! (cd "$(git rev-parse --show-toplevel)"/'apps/api' && (
set --
while IFS= read -r ward_path; do
if [ -n "$ward_path" ]; then set -- "$@" "--path=$ward_path"; fi
done <<WARD_STAGED
$(git -c core.quotePath=false diff --cached --name-only --diff-filter=ACMR --relative)
WARD_STAGED
ward check --profile pre-commit --format json "$@"
)); then
exit 1
fi
fi
# ward-owned: quality-hooks v1 end apps/api
  • pre-commit runs only when the commit stages a file under the project, and passes the staged paths relative to the project.
  • pre-push compares HEAD with the branch’s upstream and runs only when the project changed. Without an upstream, as on a branch’s first push, it always runs. The section never reads the ref list Git passes on stdin, so hook content after it still receives that list.
  • A project whose root is the repository root keeps the original unkeyed section, which runs on every commit and push.