diff --git a/CHANGELOG.md b/CHANGELOG.md
index cb86753..b6b35e2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
+### Fixed
+- The `steplock init` sample checklist and the `git-push-quality-gate` example matched the literal text `git push`, so `git -C
push` (or any global option between `git` and `push`) skipped the gate. They now match on `command_words`
+
## [0.1.0] - 2026-09-24
First published release.
diff --git a/Installation.md b/Installation.md
index f4f34c5..6756e21 100644
--- a/Installation.md
+++ b/Installation.md
@@ -101,7 +101,7 @@ Each checklist lives in its own subdirectory. The directory name is the checklis
on_event = "tool:before"
on_tool = "bash"
-match_input = "input.command.contains('git push')"
+match_input = "input.command_words.exists(x, x == 'git') && input.command_words.exists(x, x == 'push')"
reset = "session"
allow_preview_request = true
```
diff --git a/core/src/bin/main.rs b/core/src/bin/main.rs
index b3a29c1..a83f217 100644
--- a/core/src/bin/main.rs
+++ b/core/src/bin/main.rs
@@ -170,7 +170,7 @@ fn run_hook() {
const SAMPLE_CONFIG: &str = r#"on_event = "tool:before"
on_tool = "bash"
-match_input = "input.command.contains('git push')"
+match_input = "input.command_words.exists(x, x == 'git') && input.command_words.exists(x, x == 'push')"
reset = "session"
"#;
diff --git a/core/src/bin/main_tests.rs b/core/src/bin/main_tests.rs
index 1f936c6..c8f238c 100644
--- a/core/src/bin/main_tests.rs
+++ b/core/src/bin/main_tests.rs
@@ -151,7 +151,7 @@ fn init_scaffolds_sample_checklist() {
assert!(sample.join("config.toml").exists());
assert!(sample.join("flow.mmd").exists());
let cfg = fs::read_to_string(sample.join("config.toml")).unwrap();
- assert!(cfg.contains("git push"));
+ assert!(cfg.contains("command_words"));
let flow = fs::read_to_string(sample.join("flow.mmd")).unwrap();
assert!(flow.contains("stateDiagram-v2"));
}
@@ -477,3 +477,28 @@ fn hermes_session_end_cleans_global_session() {
"on_session_end must clean the global session"
);
}
+
+#[test]
+fn init_sample_blocks_git_push_with_global_options() {
+ let tmp = TempDir::new().unwrap();
+ run_init(tmp.path()).unwrap();
+ for cmd in [
+ "git -C /repo push origin main",
+ "git push",
+ "cd x && git push -q",
+ ] {
+ let stdin = claude_stdin(cmd, "s-opts");
+ let resp = run_app(stdin.as_bytes(), tmp.path(), None).unwrap();
+ assert!(
+ matches!(resp, polyhook::HookResponse::BlockResponse(_)),
+ "sample must block `{cmd}`"
+ );
+ fs::remove_dir_all(tmp.path().join(".steplock/sessions")).unwrap();
+ }
+ let stdin = claude_stdin("git status", "s-opts");
+ let resp = run_app(stdin.as_bytes(), tmp.path(), None).unwrap();
+ assert!(
+ !matches!(resp, polyhook::HookResponse::BlockResponse(_)),
+ "sample must not block unrelated git commands"
+ );
+}
diff --git a/examples/README.md b/examples/README.md
index 3965954..dc00207 100644
--- a/examples/README.md
+++ b/examples/README.md
@@ -4,7 +4,7 @@
A four-item quality checklist that gates `git push` commands.
-**Trigger**: `tool:before` on `bash` when `input.command.contains('git push')`.
+**Trigger**: `tool:before` on `bash` when the command's words include `git` and `push` (`command_words`, so `git -C dir push` and `git push` both match).
**Flow**: linear — four states in sequence.
diff --git a/examples/git-push-quality-gate/.steplock/checklists/git-push-quality-gate/config.toml b/examples/git-push-quality-gate/.steplock/checklists/git-push-quality-gate/config.toml
index f66f047..53a2e75 100644
--- a/examples/git-push-quality-gate/.steplock/checklists/git-push-quality-gate/config.toml
+++ b/examples/git-push-quality-gate/.steplock/checklists/git-push-quality-gate/config.toml
@@ -2,6 +2,6 @@
on_event = "tool:before"
on_tool = "bash"
-match_input = "input.command.contains('git push')"
+match_input = "input.command_words.exists(x, x == 'git') && input.command_words.exists(x, x == 'push')"
reset = "session"
allow_preview_request = true
diff --git a/schemas/checklist-config.schema.json b/schemas/checklist-config.schema.json
index 6214438..11ea6d3 100644
--- a/schemas/checklist-config.schema.json
+++ b/schemas/checklist-config.schema.json
@@ -29,7 +29,7 @@
"type": "string",
"description": "Optional CEL expression evaluated against the hook event. Omit to match all invocations of on_event + on_tool. Available variables: input., output., event.tool, event.event, event.caller.",
"examples": [
- "input.command.contains('git push')",
+ "input.command_words.exists(x, x == 'git') && input.command_words.exists(x, x == 'push')",
"input.command.contains('git push') && !input.command.contains('--dry-run')",
"input.command.matches('^git (push|tag)')",
"input.path.startsWith('/etc')",