jq— required at runtime byack.shpolyhook— the underlying hook transport- An AI coding tool that fires hooks (Claude Code, Cursor, Windsurf, Cline, Amp, or any polyhook-compatible tool)
Install jq if missing:
# macOS
brew install jq
# Debian/Ubuntu
apt-get install jq
# Windows (via Chocolatey)
choco install jqDownload the latest release for your platform from GitHub Releases and place it on your PATH:
# macOS/Linux — example, adjust version and platform
curl -L https://github.com/polyhook/steplock/releases/latest/download/steplock-$(uname -s)-$(uname -m).tar.gz | tar -xz
mv steplock /usr/local/bin/Verify:
steplock --versionRequires Rust (stable).
git clone https://github.com/polyhook/steplock.git
cd steplock
cargo build --release
mv target/release/steplock /usr/local/bin/# Cargo
cargo install steplockRun inside your repo root. Creates an empty .steplock/checklists/ directory and a .steplock/.gitignore. No checklist is written:
steplock initOr create manually:
mkdir -p .steplock/checklistsAdd to .gitignore — commit checklists, ignore runtime state:
.steplock/sessions/
.steplock/audit.log
Each checklist lives in its own subdirectory. The directory name is the checklist identifier.
.steplock/
└── checklists/
└── git-push-quality-gate/
├── config.toml
└── flow.mmd
config.toml — trigger and behaviour:
#:schema https://raw.githubusercontent.com/polyhook/steplock/refs/heads/main/schemas/checklist-config.schema.json
on_event = "tool:before"
on_tool = "bash"
match_input = "input.command_words.exists(x, x == 'git') && input.command_words.exists(x, x == 'push')"
reset = "session"
allow_preview_request = trueflow.mmd — checklist items as a Mermaid state diagram:
stateDiagram-v2
[*] --> clean_code
clean_code --> test_coverage
test_coverage --> documentation
documentation --> no_secrets
no_secrets --> [*]
clean_code : Did you write clean, readable code?
test_coverage: Did you increase test coverage by at least a little?
documentation: Did you update relevant documentation?
no_secrets : Did you check for hardcoded secrets or credentials?
See examples/git-push-quality-gate/ for a working reference.
Tell your AI tool to invoke steplock on hook events. Configuration varies by tool.
In your project's .claude/settings.json:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "steplock" }
]
}
]
}
}To gate every project, put the same block in ~/.claude/settings.json and add your checklists to the global steplock directory (see Global checklists). The same global checklists apply to every agent you register the hook in, such as Hermes Agent.
In ~/.hermes/config.yaml:
hooks:
pre_tool_call:
- matcher: "terminal"
command: "~/.cargo/bin/steplock"
timeout: 10
on_session_end:
- command: "~/.cargo/bin/steplock"
timeout: 10Use the full path to the binary (which steplock; ~/.cargo/bin/steplock after cargo install). Hermes finds a bare steplock on PATH at runtime, but hermes hooks doctor does not search PATH and reports script missing or not executable.
-
pre_tool_callwithmatcher: "terminal"gates shell commands. polyhook maps Hermesterminaltobash, so checklists withon_tool = "bash"work unchanged. -
on_session_endremoves the session state when the Hermes session ends. -
steplock answers in Hermes's own format (
{"action": "block", "message": ...}). -
Hermes asks once to approve each new hook command. In non-interactive contexts (gateway, cron), set
hooks_auto_accept: trueorHERMES_ACCEPT_HOOKS=1. Restart Hermes after changing hooks. -
Check the setup with
hermes hooks doctor, then fire it with a test payload:echo '{"args": {"command": "git push origin main"}, "session_id": "check"}' > /tmp/push.json hermes hooks test pre_tool_call --for-tool terminal --payload-file /tmp/push.json # parsed (Hermes wire shape): {"action": "block", "message": "[<checklist>: 1/N] ..."}
Hermes runs hooks in the directory where Hermes started, not in the terminal tool's working directory. Global checklists do not depend on this. A project .steplock/ is found only when Hermes was started inside that project.
Follow your tool's hook registration docs and point the hook command at steplock. polyhook normalises the event format — no per-tool changes needed.
Trigger a matched action from your AI tool (e.g. ask it to run git push). Expected output in the hook response:
Did you write clean, readable code?
When finished, run: sh /path/to/repo/.steplock/sessions/<session-id>/git-push-quality-gate/ack.sh
Then retry your original command.
Check the audit log for a machine-readable record:
cat .steplock/audit.logGlobal checklists apply to every project. Create the global steplock directory (with an empty checklists/):
steplock init --global
# steplock: initialized /Users/you/.config/steplock/checklistsThe directory is $STEPLOCK_GLOBAL_DIR, else $XDG_CONFIG_HOME/steplock, else ~/.config/steplock. Project checklists run first. A project checklist with the same name replaces the global one. Set STEPLOCK_GLOBAL_DIR="" to turn global checklists off.
steplock validate # check every config.toml / flow.mmd in .steplock/checklists/ and the global checklists/
steplock clean # remove all session state under .steplock/sessions/ (checklists restart fresh)
steplock clean --global # remove all session state in the global steplock directoryAdd the schema hint to config.toml (already shown above) for Taplo autocomplete and inline validation via the Even Better TOML VS Code extension.
For flow.mmd, install Mermaid Preview to get syntax highlighting and diagram preview.
Both extensions are listed in .vscode/extensions.json — VS Code prompts to install them on repo open.