@codyswann/lisa 4.8.4 → 4.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/all/copy-overwrite/scripts/lisa-gates.mjs +29 -3
- package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
- package/dist/core/lisa-owned-hash-ledger.js +3 -0
- package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
- package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
- package/dist/core/upstream-evidence-manifest.js +7 -3
- package/dist/core/upstream-evidence-manifest.js.map +1 -1
- package/dist/opencode/hooks-installer.d.ts.map +1 -1
- package/dist/opencode/hooks-installer.js +39 -17
- package/dist/opencode/hooks-installer.js.map +1 -1
- package/dist/opencode/plugin-catalog.d.ts.map +1 -1
- package/dist/opencode/plugin-catalog.js +5 -0
- package/dist/opencode/plugin-catalog.js.map +1 -1
- package/dist/opencode/plugin-templates/block-no-verify.sh +464 -0
- package/dist/opencode/plugin-templates/lisa-block-no-verify.ts +81 -0
- package/package.json +1 -1
- package/plugins/lisa/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-agy/plugin.json +1 -1
- package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-agy/plugin.json +1 -1
- package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-agy/plugin.json +1 -1
- package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-agy/plugin.json +1 -1
- package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-agy/plugin.json +1 -1
- package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-agy/plugin.json +1 -1
- package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-agy/plugin.json +1 -1
- package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-agy/plugin.json +1 -1
- package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-agy/plugin.json +1 -1
- package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
- package/scripts/copy-opencode-plugin-templates.mjs +4 -0
- package/typescript/copy-overwrite/scripts/lisa-mutation.mjs +20 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hooks-installer.d.ts","sourceRoot":"","sources":["../../src/opencode/hooks-installer.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"hooks-installer.d.ts","sourceRoot":"","sources":["../../src/opencode/hooks-installer.ts"],"names":[],"mappings":"AA2CA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAOrD,gFAAgF;AAChF,eAAO,MAAM,sBAAsB,WAAW,CAAC;AAE/C,sEAAsE;AACtE,eAAO,MAAM,wBAAwB,kBAAkB,CAAC;AAKxD,wEAAwE;AACxE,eAAO,MAAM,0BAA0B,eAAe,CAAC;AAEvD,2EAA2E;AAC3E,eAAO,MAAM,gCAAgC,oCAAoE,CAAC;AAqDlH,gDAAgD;AAChD,MAAM,WAAW,0BAA0B;IACzC,uEAAuE;IACvE,QAAQ,CAAC,YAAY,EAAE,SAAS,MAAM,EAAE,CAAC;IACzC,mEAAmE;IACnE,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,6EAA6E;IAC7E,QAAQ,CAAC,aAAa,EAAE,OAAO,CAAC;IAChC,yEAAyE;IACzE,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;CACrC;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAsB,YAAY,CAChC,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,EACf,aAAa,EAAE,SAAS,WAAW,EAAE,EACrC,oBAAoB,EAAE,SAAS,MAAM,EAAE,GACtC,OAAO,CAAC,0BAA0B,CAAC,CAyDrC;AAgGD;;;;;;;;;;;;GAYG;AACH,wBAAgB,sBAAsB,CAAC,aAAa,EAAE,MAAM,GAAG,MAAM,CAyCpE;AAiGD;;;;;GAKG;AACH,wBAAsB,wBAAwB,CAC5C,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,SAAS,MAAM,EAAE,CAAC,CAa5B"}
|
|
@@ -5,10 +5,14 @@
|
|
|
5
5
|
* `.codex/hooks.json`), OpenCode is mapped to its NATIVE surfaces first, then
|
|
6
6
|
* falls back to runtime plugins only where genuine behavior is required:
|
|
7
7
|
*
|
|
8
|
-
* - block-no-verify → `
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
8
|
+
* - block-no-verify → a `tool.execute.before` plugin running the canonical
|
|
9
|
+
* `block-no-verify.sh`, with the `permission.bash` deny globs kept beneath
|
|
10
|
+
* it as a floor. The globs alone were a SPELLING filter: they matched
|
|
11
|
+
* `--no-verify` and two `git commit -n` prefixes and missed `--no-veri`,
|
|
12
|
+
* which git resolves to the same option. Whether a token is an option or
|
|
13
|
+
* merely contains one's text is a question only a tokenizer can answer, so
|
|
14
|
+
* the semantic guard is the plugin and the globs are what survives if the
|
|
15
|
+
* plugin fails to load.
|
|
12
16
|
* - format-on-edit → OpenCode's BUILT-IN prettier formatter already formats on
|
|
13
17
|
* edit, so Lisa emits no formatter config (overriding it would be worse than
|
|
14
18
|
* the default). This is why there is no format plugin below.
|
|
@@ -48,22 +52,32 @@ export const OPENCODE_LISA_RULES_SUBDIR = "lisa-rules";
|
|
|
48
52
|
/** OpenCode instruction glob that loads Lisa eager rules every session. */
|
|
49
53
|
export const OPENCODE_EAGER_RULES_INSTRUCTION = `${OPENCODE_CONFIG_DIR}/${OPENCODE_LISA_RULES_SUBDIR}/eager/*.md`;
|
|
50
54
|
/**
|
|
51
|
-
* `permission.bash` deny patterns that
|
|
55
|
+
* `permission.bash` deny patterns that back up Lisa's `block-no-verify` guard.
|
|
52
56
|
* Each glob is matched against the parsed shell command; `*` matches any run of
|
|
53
57
|
* characters. Deny-only (no catch-all allow) so non-matching commands fall
|
|
54
58
|
* through to the host's / OpenCode's default posture.
|
|
55
59
|
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
* `
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
60
|
+
* A FLOOR, and no longer the guard. The premise this list was written under —
|
|
61
|
+
* "OpenCode gives Lisa a glob list, not a hook it can tokenize" — is false:
|
|
62
|
+
* `tool.execute.before` fires for the `bash` tool under `opencode run` and a
|
|
63
|
+
* throw cancels the call, which is what `lisa-block-no-verify.ts` now uses to
|
|
64
|
+
* run the same tokenizing script Claude, Codex, Cursor and Copilot run.
|
|
65
|
+
*
|
|
66
|
+
* What a glob list cannot do is separate a token that IS an option from a token
|
|
67
|
+
* that merely contains one's text, and that limit runs in both directions.
|
|
68
|
+
* `git commit --no-veri` is an abbreviation git accepts and no glob here
|
|
69
|
+
* matches, and no addition fixes the class, because which abbreviations git
|
|
70
|
+
* accepts is a property of git's parser; `git commit -am wip -n` reaches `-n`
|
|
71
|
+
* late on the line and cannot be matched without a glob broad enough to refuse
|
|
72
|
+
* `grep -n`. In the other direction these globs are too eager: `*--no-verify*`
|
|
73
|
+
* fires on the flag quoted inside a commit message, which the tokenizing guard
|
|
74
|
+
* correctly allows.
|
|
75
|
+
*
|
|
76
|
+
* Both costs are accepted rather than removed. A guard that is silently absent
|
|
77
|
+
* reads exactly like a guard that is passing, so if the plugin fails to load —
|
|
78
|
+
* a Bun transpile error, a run that skips project plugins — the literal
|
|
79
|
+
* spellings are still refused here. A reworded commit message is a cheaper
|
|
80
|
+
* failure than an unguarded `--no-verify`.
|
|
67
81
|
*/
|
|
68
82
|
const NO_VERIFY_DENY_PATTERNS = {
|
|
69
83
|
"*--no-verify*": "deny",
|
|
@@ -73,8 +87,16 @@ const NO_VERIFY_DENY_PATTERNS = {
|
|
|
73
87
|
"*HUSKY_SKIP_HOOKS=*": "deny",
|
|
74
88
|
"*core.hooksPath*/dev/null*": "deny",
|
|
75
89
|
};
|
|
76
|
-
/**
|
|
90
|
+
/**
|
|
91
|
+
* Canonical policy files used by universal OpenCode adapters.
|
|
92
|
+
*
|
|
93
|
+
* Kept in step with `canonicalSupportFiles` in
|
|
94
|
+
* `scripts/copy-opencode-plugin-templates.mjs`, which stages the same list into
|
|
95
|
+
* `dist/` for the packaged installer. A file listed here and not there resolves
|
|
96
|
+
* in a source checkout and is missing from every installed copy.
|
|
97
|
+
*/
|
|
77
98
|
const PLUGIN_SUPPORT_FILES = [
|
|
99
|
+
"block-no-verify.sh",
|
|
78
100
|
"parity-safety-net.sh",
|
|
79
101
|
"parity-safety-net-heredoc.py",
|
|
80
102
|
];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hooks-installer.js","sourceRoot":"","sources":["../../src/opencode/hooks-installer.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"hooks-installer.js","sourceRoot":"","sources":["../../src/opencode/hooks-installer.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin-catalog.d.ts","sourceRoot":"","sources":["../../src/opencode/plugin-catalog.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAErD,iDAAiD;AACjD,MAAM,WAAW,kBAAkB;IACjC,sEAAsE;IACtE,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,8EAA8E;IAC9E,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;IAClC,0EAA0E;IAC1E,QAAQ,CAAC,eAAe,EAAE,SAAS,CAAC,WAAW,GAAG,GAAG,CAAC,EAAE,CAAC;CAC1D;AAED;;;GAGG;AACH,eAAO,MAAM,cAAc,EAAE,SAAS,kBAAkB,
|
|
1
|
+
{"version":3,"file":"plugin-catalog.d.ts","sourceRoot":"","sources":["../../src/opencode/plugin-catalog.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAErD,iDAAiD;AACjD,MAAM,WAAW,kBAAkB;IACjC,sEAAsE;IACtE,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,8EAA8E;IAC9E,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;IAClC,0EAA0E;IAC1E,QAAQ,CAAC,eAAe,EAAE,SAAS,CAAC,WAAW,GAAG,GAAG,CAAC,EAAE,CAAC;CAC1D;AAED;;;GAGG;AACH,eAAO,MAAM,cAAc,EAAE,SAAS,kBAAkB,EAmDvD,CAAC"}
|
|
@@ -8,6 +8,11 @@ export const PLUGIN_CATALOG = [
|
|
|
8
8
|
templateFilename: "lisa-parity-safety-net.ts",
|
|
9
9
|
forProjectTypes: ["*"],
|
|
10
10
|
},
|
|
11
|
+
{
|
|
12
|
+
id: "block-no-verify",
|
|
13
|
+
templateFilename: "lisa-block-no-verify.ts",
|
|
14
|
+
forProjectTypes: ["*"],
|
|
15
|
+
},
|
|
11
16
|
{
|
|
12
17
|
id: "session-bootstrap",
|
|
13
18
|
templateFilename: "lisa-session-bootstrap.ts",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin-catalog.js","sourceRoot":"","sources":["../../src/opencode/plugin-catalog.ts"],"names":[],"mappings":"AAqBA;;;GAGG;AACH,MAAM,CAAC,MAAM,cAAc,GAAkC;IAC3D;QACE,EAAE,EAAE,mBAAmB;QACvB,gBAAgB,EAAE,2BAA2B;QAC7C,eAAe,EAAE,CAAC,GAAG,CAAC;KACvB;IACD;QACE,EAAE,EAAE,mBAAmB;QACvB,gBAAgB,EAAE,2BAA2B;QAC7C,eAAe,EAAE,CAAC,GAAG,CAAC;KACvB;IACD;QACE,EAAE,EAAE,8BAA8B;QAClC,gBAAgB,EAAE,sCAAsC;QACxD,eAAe,EAAE,CAAC,GAAG,CAAC;KACvB;IACD;QACE,EAAE,EAAE,2BAA2B;QAC/B,gBAAgB,EAAE,mCAAmC;QACrD,eAAe,EAAE,CAAC,GAAG,CAAC;KACvB;IACD;QACE,EAAE,EAAE,2BAA2B;QAC/B,gBAAgB,EAAE,mCAAmC;QACrD,eAAe,EAAE,CAAC,YAAY,CAAC;KAChC;IACD;QACE,EAAE,EAAE,cAAc;QAClB,gBAAgB,EAAE,sBAAsB;QACxC,eAAe,EAAE,CAAC,YAAY,CAAC;KAChC;IACD;QACE,EAAE,EAAE,iBAAiB;QACrB,gBAAgB,EAAE,yBAAyB;QAC3C,eAAe,EAAE,CAAC,YAAY,EAAE,OAAO,CAAC;KACzC;IACD;QACE,EAAE,EAAE,uBAAuB;QAC3B,gBAAgB,EAAE,+BAA+B;QACjD,eAAe,EAAE,CAAC,QAAQ,CAAC;KAC5B;IACD;QACE,EAAE,EAAE,iBAAiB;QACrB,gBAAgB,EAAE,yBAAyB;QAC3C,eAAe,EAAE,CAAC,OAAO,CAAC;KAC3B;CACF,CAAC"}
|
|
1
|
+
{"version":3,"file":"plugin-catalog.js","sourceRoot":"","sources":["../../src/opencode/plugin-catalog.ts"],"names":[],"mappings":"AAqBA;;;GAGG;AACH,MAAM,CAAC,MAAM,cAAc,GAAkC;IAC3D;QACE,EAAE,EAAE,mBAAmB;QACvB,gBAAgB,EAAE,2BAA2B;QAC7C,eAAe,EAAE,CAAC,GAAG,CAAC;KACvB;IACD;QACE,EAAE,EAAE,iBAAiB;QACrB,gBAAgB,EAAE,yBAAyB;QAC3C,eAAe,EAAE,CAAC,GAAG,CAAC;KACvB;IACD;QACE,EAAE,EAAE,mBAAmB;QACvB,gBAAgB,EAAE,2BAA2B;QAC7C,eAAe,EAAE,CAAC,GAAG,CAAC;KACvB;IACD;QACE,EAAE,EAAE,8BAA8B;QAClC,gBAAgB,EAAE,sCAAsC;QACxD,eAAe,EAAE,CAAC,GAAG,CAAC;KACvB;IACD;QACE,EAAE,EAAE,2BAA2B;QAC/B,gBAAgB,EAAE,mCAAmC;QACrD,eAAe,EAAE,CAAC,GAAG,CAAC;KACvB;IACD;QACE,EAAE,EAAE,2BAA2B;QAC/B,gBAAgB,EAAE,mCAAmC;QACrD,eAAe,EAAE,CAAC,YAAY,CAAC;KAChC;IACD;QACE,EAAE,EAAE,cAAc;QAClB,gBAAgB,EAAE,sBAAsB;QACxC,eAAe,EAAE,CAAC,YAAY,CAAC;KAChC;IACD;QACE,EAAE,EAAE,iBAAiB;QACrB,gBAAgB,EAAE,yBAAyB;QAC3C,eAAe,EAAE,CAAC,YAAY,EAAE,OAAO,CAAC;KACzC;IACD;QACE,EAAE,EAAE,uBAAuB;QAC3B,gBAAgB,EAAE,+BAA+B;QACjD,eAAe,EAAE,CAAC,QAAQ,CAAC;KAC5B;IACD;QACE,EAAE,EAAE,iBAAiB;QACrB,gBAAgB,EAAE,yBAAyB;QAC3C,eAAe,EAAE,CAAC,OAAO,CAAC;KAC3B;CACF,CAAC"}
|
|
@@ -0,0 +1,464 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# PreToolUse hook for Bash: blocks commands that bypass git's verification hooks.
|
|
3
|
+
# Bypassing pre-commit/pre-push hooks (which exist for a reason) is blocked in
|
|
4
|
+
# all of its forms; the fix is to address the underlying issue, not silence the
|
|
5
|
+
# check. See feedback_never_no_verify in user memory.
|
|
6
|
+
#
|
|
7
|
+
# Blocked bypass vectors:
|
|
8
|
+
# 1. --no-verify, and any prefix abbreviation git would accept for it;
|
|
9
|
+
# 2. HUSKY=0 / HUSKY_SKIP_HOOKS=... — disables husky-managed git hooks;
|
|
10
|
+
# 3. core.hooksPath pointed anywhere but an allowlisted in-repo hooks dir;
|
|
11
|
+
# 4. --config-env=core.hooksPath=VAR, in both the one- and two-token spellings;
|
|
12
|
+
# 5. GIT_CONFIG_KEY_<n>=core.hooksPath env-var-style command-scope config;
|
|
13
|
+
# 6. the short `-n`, as a real argv token of a `git commit` — bare, or bundled
|
|
14
|
+
# into a short-option cluster such as `-nm "msg"`.
|
|
15
|
+
#
|
|
16
|
+
# The line below is what lets `lisa apply` tell a downstream copy of this guard
|
|
17
|
+
# that is BEHIND from one that is AHEAD. Byte comparison cannot: both look like
|
|
18
|
+
# "differs from mine", and guessing "behind" is how a fork's stronger guard gets
|
|
19
|
+
# silently replaced by a weaker upstream one. Refresh compares the two declared
|
|
20
|
+
# sets, and refuses to overwrite a copy declaring anything this one does not.
|
|
21
|
+
#
|
|
22
|
+
# Add a name here in the same commit that closes a vector. A hardening that
|
|
23
|
+
# forgets to is invisible to refresh, and shows up as an unexplained diff at
|
|
24
|
+
# review time instead of a named capability.
|
|
25
|
+
# lisa-guard-capabilities: no-verify-abbrev, husky-env, hookspath-allowlist, config-env, git-config-key, no-verify-short
|
|
26
|
+
#
|
|
27
|
+
# Shell-token matching avoids false positives from issue bodies, heredocs, and
|
|
28
|
+
# commit-message prose while still catching quoted real argv values such as
|
|
29
|
+
# `git -c "core.hooksPath=/dev/null"`.
|
|
30
|
+
#
|
|
31
|
+
# The short `-n` form used to be excluded here, and the stated reason was that
|
|
32
|
+
# "grep cannot distinguish a real -n option from -n in commit-message prose or
|
|
33
|
+
# an unrelated piped command". That was true of the matcher it was written for.
|
|
34
|
+
# It is not true of this one: the command is TOKENIZED, not grepped, so `-m "fix
|
|
35
|
+
# the -n flag"` is one token holding the message and `grep -n` is a separate
|
|
36
|
+
# command — the two cases the comment called inseparable are separated by the
|
|
37
|
+
# same machinery `--config-env` and `GIT_CONFIG_KEY_<n>` already depend on.
|
|
38
|
+
#
|
|
39
|
+
# A rationale that outlives the implementation it describes is worse than none:
|
|
40
|
+
# it reads as a considered decision and stops the next reader from re-examining
|
|
41
|
+
# it. Measured against real git, `git commit -n`, `-nm msg`, and `-anm msg` all
|
|
42
|
+
# skip pre-commit exactly as completely as `--no-verify`, and `-nm` is the more
|
|
43
|
+
# likely spelling in practice because it reads as an ordinary message flag.
|
|
44
|
+
#
|
|
45
|
+
# What the old comment got right is the part about `-n` being common, and that
|
|
46
|
+
# concern is answered by SCOPE rather than by giving up: `-n` is `--dry-run` for
|
|
47
|
+
# `push`, `--no-stat` for `merge`, and a line count for head/tail/sort/grep, so
|
|
48
|
+
# the match is confined to the argv of a `git commit` invocation and nothing
|
|
49
|
+
# else. See `git_commit_skips_verification` below.
|
|
50
|
+
set -euo pipefail
|
|
51
|
+
|
|
52
|
+
input="$(cat)"
|
|
53
|
+
|
|
54
|
+
# Both interpreters must be probed BEFORE they are used, and a missing one must
|
|
55
|
+
# be announced rather than swallowed.
|
|
56
|
+
#
|
|
57
|
+
# `jq` used to be called unguarded here while `python3` two lines below was
|
|
58
|
+
# guarded. Under `set -e` an absent jq aborted the script with 127, and Claude
|
|
59
|
+
# Code treats any non-2 exit as a NON-BLOCKING hook error — so the hook that
|
|
60
|
+
# enforces "never --no-verify" silently permitted the very thing it exists to
|
|
61
|
+
# stop. It was not hypothetical: agent containers routinely ship no jq (that is
|
|
62
|
+
# why jq is now a pinned toolchain entry). The Codex variant of this script has
|
|
63
|
+
# always had the guard, so this was a parity gap rather than a design choice.
|
|
64
|
+
#
|
|
65
|
+
# Degrading to "allow" is still the right behaviour — a hook that cannot parse
|
|
66
|
+
# its input cannot tell a bypass from an ordinary command, and failing closed
|
|
67
|
+
# would block every Bash call on a machine missing an interpreter. What is NOT
|
|
68
|
+
# right is doing it quietly, so the operator gets one line on stderr saying the
|
|
69
|
+
# protection is off. A guard that is silently absent reads exactly like a guard
|
|
70
|
+
# that is passing.
|
|
71
|
+
for required in jq python3; do
|
|
72
|
+
if ! command -v "$required" >/dev/null 2>&1; then
|
|
73
|
+
printf 'block-no-verify: %s not found; --no-verify protection is NOT active\n' \
|
|
74
|
+
"$required" >&2
|
|
75
|
+
exit 0
|
|
76
|
+
fi
|
|
77
|
+
done
|
|
78
|
+
|
|
79
|
+
tool_name="$(printf '%s' "$input" | jq -r '.tool_name // empty' 2>/dev/null || true)"
|
|
80
|
+
if [ "$tool_name" != "Bash" ]; then
|
|
81
|
+
exit 0
|
|
82
|
+
fi
|
|
83
|
+
|
|
84
|
+
command_str="$(printf '%s' "$input" | jq -r '.tool_input.command // empty' 2>/dev/null || true)"
|
|
85
|
+
if [ -z "$command_str" ]; then
|
|
86
|
+
exit 0
|
|
87
|
+
fi
|
|
88
|
+
|
|
89
|
+
if ! BLOCK_NO_VERIFY_COMMAND="$command_str" python3 - <<'PY'
|
|
90
|
+
import os
|
|
91
|
+
import re
|
|
92
|
+
import shlex
|
|
93
|
+
import sys
|
|
94
|
+
|
|
95
|
+
command = os.environ.get("BLOCK_NO_VERIFY_COMMAND", "")
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def strip_heredocs(text: str) -> str:
|
|
99
|
+
lines = text.splitlines()
|
|
100
|
+
output = []
|
|
101
|
+
pending = []
|
|
102
|
+
marker_pattern = re.compile(
|
|
103
|
+
r"<<-?\s*(?:'([^']+)'|\"([^\"]+)\"|([A-Za-z_][A-Za-z0-9_]*))"
|
|
104
|
+
)
|
|
105
|
+
index = 0
|
|
106
|
+
while index < len(lines):
|
|
107
|
+
line = lines[index]
|
|
108
|
+
output.append(line)
|
|
109
|
+
pending.extend(
|
|
110
|
+
next(group for group in match.groups() if group)
|
|
111
|
+
for match in marker_pattern.finditer(line)
|
|
112
|
+
)
|
|
113
|
+
index += 1
|
|
114
|
+
while pending and index < len(lines):
|
|
115
|
+
if lines[index].strip() == pending[0]:
|
|
116
|
+
output.append(lines[index])
|
|
117
|
+
pending.pop(0)
|
|
118
|
+
index += 1
|
|
119
|
+
break
|
|
120
|
+
index += 1
|
|
121
|
+
return "\n".join(output)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
try:
|
|
125
|
+
tokens = shlex.split(strip_heredocs(command), posix=True)
|
|
126
|
+
except ValueError:
|
|
127
|
+
sys.exit(0)
|
|
128
|
+
|
|
129
|
+
normalized_tokens = [token.strip("();|&") for token in tokens]
|
|
130
|
+
|
|
131
|
+
# The only relocations that keep a repo's own hooks in play. `.husky` is the
|
|
132
|
+
# husky convention this fleet runs; `.githooks` is the common hand-rolled one.
|
|
133
|
+
# Anything else — including "" and /dev/null, which are simply the two most
|
|
134
|
+
# obvious members of the blocked set rather than special cases — is refused.
|
|
135
|
+
PERMITTED_HOOKS_PATHS = {".husky", ".githooks"}
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def is_permitted_hooks_path(value):
|
|
139
|
+
"""Whether a core.hooksPath value relocates hooks rather than disabling them.
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
value: The raw core.hooksPath value as it appeared on the command line.
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
True if the path is an established in-repo hooks directory.
|
|
146
|
+
"""
|
|
147
|
+
cleaned = value.strip().strip("'\"")
|
|
148
|
+
if cleaned.startswith("./"):
|
|
149
|
+
cleaned = cleaned[2:]
|
|
150
|
+
return cleaned.rstrip("/") in PERMITTED_HOOKS_PATHS
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
# Git resolves any UNAMBIGUOUS abbreviation of a long option, so `--no-verify`
|
|
154
|
+
# is only the longest of the spellings that disable verification: `git commit
|
|
155
|
+
# --no-veri` skips hooks exactly as completely. An equality check therefore
|
|
156
|
+
# enforced the guard against the one spelling nobody in a hurry types.
|
|
157
|
+
#
|
|
158
|
+
# Matched as "a prefix of --no-verify" rather than by listing abbreviations,
|
|
159
|
+
# because the set of accepted abbreviations is a property of git's parser and
|
|
160
|
+
# changes with the surrounding options. `--no-verbose` is NOT caught, and must
|
|
161
|
+
# not be: it diverges from `--no-verify` at the character after `--no-ver`, so
|
|
162
|
+
# it fails the prefix test.
|
|
163
|
+
#
|
|
164
|
+
# The floor is `--no-v`. Shorter is refused anyway — bare `--no-` is not a flag
|
|
165
|
+
# — and blocking an abbreviation git would reject as ambiguous costs nothing,
|
|
166
|
+
# while missing one git accepts costs the whole guard.
|
|
167
|
+
NO_VERIFY = "--no-verify"
|
|
168
|
+
NO_VERIFY_MIN_PREFIX = len("--no-v")
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def disables_verification(token):
|
|
172
|
+
"""Whether a token is `--no-verify` or an abbreviation git would accept.
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
token: A single shell token from the command line.
|
|
176
|
+
|
|
177
|
+
Returns:
|
|
178
|
+
True if git would read this token as --no-verify.
|
|
179
|
+
"""
|
|
180
|
+
return (
|
|
181
|
+
len(token) >= NO_VERIFY_MIN_PREFIX
|
|
182
|
+
and len(token) <= len(NO_VERIFY)
|
|
183
|
+
and NO_VERIFY.startswith(token)
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
# `git commit -n` is the short spelling of --no-verify and skips pre-commit and
|
|
188
|
+
# commit-msg identically. Matching it needs one thing the `--no-verify` scan
|
|
189
|
+
# above does not: SCOPE. `-n` means --dry-run to `git push`, --no-stat to `git
|
|
190
|
+
# merge`, and a line count to head/tail/sort/grep, so a token-anywhere match
|
|
191
|
+
# would refuse ordinary commands all day. Only `git commit` reads it as a hook
|
|
192
|
+
# bypass, so only that invocation's argv is scanned.
|
|
193
|
+
#
|
|
194
|
+
# The tokenizer below is a SECOND pass, deliberately not shared with the one the
|
|
195
|
+
# checks above use. `shlex.split` leaves `|` and `;` glued to their neighbours,
|
|
196
|
+
# which those checks paper over by stripping the punctuation off each token —
|
|
197
|
+
# fine when the question is "does this token look like a bypass anywhere in the
|
|
198
|
+
# line", fatal when the question is "where does this command end", because
|
|
199
|
+
# `git commit -m x && grep -n foo` would then read as one long git invocation
|
|
200
|
+
# and refuse the grep. `punctuation_chars=True` emits the operators as their own
|
|
201
|
+
# tokens, so the invocation's boundary is a token the scan can stop at.
|
|
202
|
+
COMMAND_SEPARATORS = {
|
|
203
|
+
";", "|", "||", "&", "&&", "(", ")", "<", ">", ">>", "<<", "&|",
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
# git's own options that take a SEPARATE value token, which therefore must be
|
|
207
|
+
# skipped when looking for the subcommand: `git -c core.hooksPath=x commit` and
|
|
208
|
+
# `git -C /repo commit` both reach `commit`.
|
|
209
|
+
GIT_GLOBAL_SEPARATE_VALUE = {
|
|
210
|
+
"-c", "-C", "--config-env", "--git-dir", "--work-tree",
|
|
211
|
+
"--namespace", "--exec-path", "--super-prefix",
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
# `git commit` long options whose value can be a separate token. Listed so the
|
|
215
|
+
# VALUE is never mistaken for a flag — `git commit --author "A -n B" -m x`
|
|
216
|
+
# must stay allowed. Only genuinely value-taking options belong here: adding a
|
|
217
|
+
# boolean one by mistake would swallow the token after it, and `git commit
|
|
218
|
+
# --amend -n` would go unnoticed.
|
|
219
|
+
COMMIT_LONG_SEPARATE_VALUE = {
|
|
220
|
+
"--message", "--file", "--author", "--date", "--template", "--cleanup",
|
|
221
|
+
"--reuse-message", "--reedit-message", "--fixup", "--squash",
|
|
222
|
+
"--pathspec-from-file", "--trailer",
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
# Short `git commit` options that REQUIRE a value: `-m`/`-F`/`-c`/`-C`/`-t`.
|
|
226
|
+
# In a cluster the value is whatever follows them in the same token, or the next
|
|
227
|
+
# token when nothing does — which is why `-mn` is the message "n" and not a
|
|
228
|
+
# bypass, and `-nm msg` IS one.
|
|
229
|
+
COMMIT_SHORT_REQUIRED_VALUE = set("mFcCt")
|
|
230
|
+
|
|
231
|
+
# Short options taking an OPTIONAL value, which git only ever reads attached:
|
|
232
|
+
# `-uno` is --untracked-files=no, `-Skeyid` is --gpg-sign=keyid. The cluster
|
|
233
|
+
# ends at them either way, so `-un` is untracked-files "n" rather than a bypass.
|
|
234
|
+
COMMIT_SHORT_OPTIONAL_VALUE = set("uS")
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def line_boundaries_as_separators(text):
|
|
238
|
+
"""Turn newlines into command separators, keeping continuations intact.
|
|
239
|
+
|
|
240
|
+
A newline ends a command exactly as `;` does, but shlex treats it as plain
|
|
241
|
+
whitespace — so `git commit -m x` on one line and `grep -n foo` on the next
|
|
242
|
+
read as ONE invocation and the grep gets refused. That is the same
|
|
243
|
+
false-positive class the short-form match exists to avoid, arriving through
|
|
244
|
+
the boundary rather than through the token.
|
|
245
|
+
|
|
246
|
+
Backslash-newline is joined first, because there the newline is NOT a
|
|
247
|
+
boundary: `git commit \\` + newline + `-nm x` is one command, and turning
|
|
248
|
+
that newline into a separator would hide a real bypass.
|
|
249
|
+
|
|
250
|
+
Args:
|
|
251
|
+
text: The command line, heredoc payloads already stripped.
|
|
252
|
+
|
|
253
|
+
Returns:
|
|
254
|
+
The same command with line breaks spelled as separators.
|
|
255
|
+
"""
|
|
256
|
+
joined = text.replace("\r\n", "\n").replace("\\\n", " ")
|
|
257
|
+
return joined.replace("\n", " ; ")
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def shell_tokens(text):
|
|
261
|
+
"""Tokenize a command with shell operators kept as their own tokens.
|
|
262
|
+
|
|
263
|
+
Args:
|
|
264
|
+
text: The command line, heredoc payloads already stripped.
|
|
265
|
+
|
|
266
|
+
Returns:
|
|
267
|
+
The token list, with `;`, `|`, `&&`, `(` and friends standing alone.
|
|
268
|
+
"""
|
|
269
|
+
lexer = shlex.shlex(
|
|
270
|
+
line_boundaries_as_separators(text), posix=True, punctuation_chars=True
|
|
271
|
+
)
|
|
272
|
+
lexer.whitespace_split = True
|
|
273
|
+
# shlex treats `#` as a comment introducer by default and would silently
|
|
274
|
+
# truncate the rest of the line; `shlex.split` disables it, and so must this.
|
|
275
|
+
lexer.commenters = ""
|
|
276
|
+
return list(lexer)
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def cluster_skips_verification(cluster):
|
|
280
|
+
"""Read a short-option cluster the way git's own parser reads it.
|
|
281
|
+
|
|
282
|
+
Args:
|
|
283
|
+
cluster: A single token beginning with one `-`, e.g. `-nm` or `-mn`.
|
|
284
|
+
|
|
285
|
+
Returns:
|
|
286
|
+
A pair (bypasses, consumes_next_token). `bypasses` is True when a real
|
|
287
|
+
`-n` option is present; `consumes_next_token` is True when the cluster
|
|
288
|
+
ends in a value-taking option whose value is the following token.
|
|
289
|
+
"""
|
|
290
|
+
body = cluster[1:]
|
|
291
|
+
for offset, letter in enumerate(body):
|
|
292
|
+
if letter == "n":
|
|
293
|
+
return (True, False)
|
|
294
|
+
if letter in COMMIT_SHORT_REQUIRED_VALUE:
|
|
295
|
+
# Everything after this letter is the value. It is a separate token
|
|
296
|
+
# only when nothing is attached.
|
|
297
|
+
return (False, offset == len(body) - 1)
|
|
298
|
+
if letter in COMMIT_SHORT_OPTIONAL_VALUE:
|
|
299
|
+
return (False, False)
|
|
300
|
+
return (False, False)
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def commit_bypasses_verification(argv):
|
|
304
|
+
"""Whether a `git commit` invocation's argv carries a real short `-n`.
|
|
305
|
+
|
|
306
|
+
Args:
|
|
307
|
+
argv: Tokens following the `commit` subcommand, to the end of the line.
|
|
308
|
+
|
|
309
|
+
Returns:
|
|
310
|
+
True if git would read one of them as --no-verify.
|
|
311
|
+
"""
|
|
312
|
+
index = 0
|
|
313
|
+
while index < len(argv):
|
|
314
|
+
token = argv[index]
|
|
315
|
+
index += 1
|
|
316
|
+
# `--` ends the options; everything after it is a pathspec, and a file
|
|
317
|
+
# legitimately named `-n` is not a bypass.
|
|
318
|
+
if token in COMMAND_SEPARATORS or token == "--":
|
|
319
|
+
return False
|
|
320
|
+
if token.startswith("--"):
|
|
321
|
+
if token in COMMIT_LONG_SEPARATE_VALUE:
|
|
322
|
+
index += 1
|
|
323
|
+
continue
|
|
324
|
+
# A bare `-` is git's stdin placeholder, not an option cluster.
|
|
325
|
+
if not token.startswith("-") or token == "-":
|
|
326
|
+
continue
|
|
327
|
+
bypasses, consumes_next = cluster_skips_verification(token)
|
|
328
|
+
if bypasses:
|
|
329
|
+
return True
|
|
330
|
+
if consumes_next:
|
|
331
|
+
index += 1
|
|
332
|
+
return False
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def subcommand_after_git(tokens, start):
|
|
336
|
+
"""Find the subcommand a `git` token introduces, and its argv.
|
|
337
|
+
|
|
338
|
+
Args:
|
|
339
|
+
tokens: The full operator-aware token list.
|
|
340
|
+
start: Index just past the `git` token.
|
|
341
|
+
|
|
342
|
+
Returns:
|
|
343
|
+
A pair (subcommand, argv), or None when the invocation names none.
|
|
344
|
+
"""
|
|
345
|
+
index = start
|
|
346
|
+
while index < len(tokens):
|
|
347
|
+
token = tokens[index]
|
|
348
|
+
if token in COMMAND_SEPARATORS:
|
|
349
|
+
return None
|
|
350
|
+
if not token.startswith("-"):
|
|
351
|
+
return (token, tokens[index + 1:])
|
|
352
|
+
index += 2 if token in GIT_GLOBAL_SEPARATE_VALUE else 1
|
|
353
|
+
return None
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def git_commit_skips_verification(text):
|
|
357
|
+
"""Whether the command runs `git commit` with the short `-n` bypass.
|
|
358
|
+
|
|
359
|
+
Args:
|
|
360
|
+
text: The command line, heredoc payloads already stripped.
|
|
361
|
+
|
|
362
|
+
Returns:
|
|
363
|
+
True if any `git commit` invocation on the line skips verification.
|
|
364
|
+
"""
|
|
365
|
+
try:
|
|
366
|
+
scoped_tokens = shell_tokens(text)
|
|
367
|
+
except ValueError:
|
|
368
|
+
return False
|
|
369
|
+
for index, token in enumerate(scoped_tokens):
|
|
370
|
+
# `/usr/bin/git` and a bare `git` are the same program; an env prefix
|
|
371
|
+
# (`HUSKY=1 git commit -n`) simply sits in an earlier token.
|
|
372
|
+
if token != "git" and not token.endswith("/git"):
|
|
373
|
+
continue
|
|
374
|
+
found = subcommand_after_git(scoped_tokens, index + 1)
|
|
375
|
+
if found and found[0] == "commit" and commit_bypasses_verification(found[1]):
|
|
376
|
+
return True
|
|
377
|
+
return False
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
if git_commit_skips_verification(strip_heredocs(command)):
|
|
381
|
+
sys.exit(1)
|
|
382
|
+
|
|
383
|
+
for i, token in enumerate(normalized_tokens):
|
|
384
|
+
if disables_verification(token):
|
|
385
|
+
sys.exit(1)
|
|
386
|
+
if token == "HUSKY=0" or token.startswith("HUSKY_SKIP_HOOKS="):
|
|
387
|
+
sys.exit(1)
|
|
388
|
+
# Allowlist the destinations, do not denylist the disabling ones.
|
|
389
|
+
#
|
|
390
|
+
# This used to block only "" and /dev/null. But hooks are disabled just as
|
|
391
|
+
# completely by pointing hooksPath at any directory that happens to contain
|
|
392
|
+
# none — `-c core.hooksPath=/tmp/empty` bypassed every hook while reading as
|
|
393
|
+
# an ordinary path — so a denylist of "obviously disabling" values can
|
|
394
|
+
# always be stepped around by naming a third thing. The set of paths that
|
|
395
|
+
# DISABLE hooks is unbounded; the set that legitimately relocates them is
|
|
396
|
+
# tiny and known, so the allowlist is the only side that can be enumerated.
|
|
397
|
+
#
|
|
398
|
+
# Matched case-insensitively because git config variable names are:
|
|
399
|
+
# `CORE.HOOKSPATH=/x` and `core.hookspath=/x` are the same setting to git,
|
|
400
|
+
# so a case-sensitive check is bypassed by holding down shift.
|
|
401
|
+
lowered = token.lower()
|
|
402
|
+
if lowered.startswith("core.hookspath="):
|
|
403
|
+
if not is_permitted_hooks_path(token.split("=", 1)[1]):
|
|
404
|
+
sys.exit(1)
|
|
405
|
+
if lowered == "core.hookspath" and i + 1 < len(normalized_tokens):
|
|
406
|
+
if not is_permitted_hooks_path(normalized_tokens[i + 1]):
|
|
407
|
+
sys.exit(1)
|
|
408
|
+
# `git --config-env=core.hooksPath=SOMEVAR` sets the same config, reading
|
|
409
|
+
# the value out of the named environment variable. The path therefore is
|
|
410
|
+
# not in the command at all, so there is nothing to allowlist against —
|
|
411
|
+
# SOMEVAR can hold anything by the time git reads it. Any core.hooksPath
|
|
412
|
+
# routed through --config-env is refused outright.
|
|
413
|
+
if lowered.startswith("--config-env="):
|
|
414
|
+
spec = token.split("=", 1)[1]
|
|
415
|
+
if spec.split("=", 1)[0].strip().lower() == "core.hookspath":
|
|
416
|
+
sys.exit(1)
|
|
417
|
+
# git accepts `--config-env <name>=<envvar>` as TWO tokens as well as one,
|
|
418
|
+
# and guarding only the `=` spelling was worse than missing the separate
|
|
419
|
+
# form outright: the trailing `core.hooksPath=.husky` then fell through to
|
|
420
|
+
# the allowlist above, which reads `.husky` as a PATH and permits it. But
|
|
421
|
+
# here it is an ENVIRONMENT VARIABLE NAME, and `env '.husky=/dev/null' git
|
|
422
|
+
# --config-env core.hooksPath=.husky` really does resolve hooksPath to
|
|
423
|
+
# /dev/null. The allowlist was being used as the bypass.
|
|
424
|
+
#
|
|
425
|
+
# Checked at the `--config-env` token, which the loop reaches first, so the
|
|
426
|
+
# refusal happens before the value token can be mistaken for a path.
|
|
427
|
+
if lowered == "--config-env" and i + 1 < len(normalized_tokens):
|
|
428
|
+
spec = normalized_tokens[i + 1]
|
|
429
|
+
if spec.split("=", 1)[0].strip().strip("'\"").lower() == "core.hookspath":
|
|
430
|
+
sys.exit(1)
|
|
431
|
+
# `GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=core.hooksPath
|
|
432
|
+
# GIT_CONFIG_VALUE_0=/dev/null git commit` sets command-scope config the
|
|
433
|
+
# same way `-c core.hooksPath=...` does — env-var-style assignments ahead of
|
|
434
|
+
# the invocation instead of a flag — so it disables every hook just as
|
|
435
|
+
# completely while matching none of the token shapes above. Upstream missed
|
|
436
|
+
# this until a downstream fork hardened its own copy against it, which is
|
|
437
|
+
# the one direction a guard must never be caught in.
|
|
438
|
+
#
|
|
439
|
+
# The index is matched as `\d+` rather than pinned to 0: git accepts any
|
|
440
|
+
# index below GIT_CONFIG_COUNT, so a single-index check is evaded by typing
|
|
441
|
+
# a 1. Refused outright, like --config-env=, because the path lives in a
|
|
442
|
+
# separate GIT_CONFIG_VALUE_<n> token that can be exported earlier,
|
|
443
|
+
# reordered, or left out entirely — there is nothing here to allowlist
|
|
444
|
+
# against.
|
|
445
|
+
key_match = re.match(r"git_config_key_\d+=(.*)$", lowered, re.DOTALL)
|
|
446
|
+
if key_match and key_match.group(1).strip().strip("'\"") == "core.hookspath":
|
|
447
|
+
sys.exit(1)
|
|
448
|
+
|
|
449
|
+
sys.exit(0)
|
|
450
|
+
PY
|
|
451
|
+
then
|
|
452
|
+
cat >&2 <<'EOF'
|
|
453
|
+
Blocked: this command bypasses pre-commit/pre-push hooks (--no-verify or its
|
|
454
|
+
short form -n, HUSKY=0, or core.hooksPath disabling). `git commit -n` and a
|
|
455
|
+
cluster such as `-nm "msg"` skip pre-commit exactly as completely as the long
|
|
456
|
+
flag. Fix the underlying issue (security audit, lint,
|
|
457
|
+
typecheck, tests, formatting) instead. If a fix is genuinely impossible, ask the
|
|
458
|
+
user to make the risk-acceptance decision and add a specific documented ignore;
|
|
459
|
+
never bypass the hook.
|
|
460
|
+
EOF
|
|
461
|
+
exit 2
|
|
462
|
+
fi
|
|
463
|
+
|
|
464
|
+
exit 0
|