no-yolo-commits 1.2.1 → 1.3.0

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/README.md CHANGED
@@ -17,6 +17,10 @@ One command. On every commit from now on:
17
17
  2. **An AI actually reads your diff** (via the [`claude`](https://claude.com/claude-code) CLI) and blocks the commit — but only for real, high-confidence problems. Not vibes, not "you could refactor this." If the reviewer is missing, slow, or having a bad day, it fails **open** — a flaky reviewer should never be the reason your commit is stuck. Skipped automatically when everything staged is a lockfile or generated asset, so bumping `package-lock.json` doesn't cost you a 2-minute wait.
18
18
  3. **`main`/`master` become look-but-don't-touch.** Commit straight there and instead of yelling at you, it just makes you a branch — `ACME-1788716508-fix-the-thing-you-were-actually-fixing`, ready to go, commit already on it. It even wrote the branch name from your diff.
19
19
 
20
+ And on every `git push`:
21
+
22
+ 4. **Pushing to `main`/`master` is refused, full stop** — independent of #3 above. The auto-branch step only stops a *direct commit* from landing on a protected branch; nothing stops you from merging that branch back locally and pushing anyway a minute later, which lands you in exactly the state #3 was trying to prevent. This hook checks the actual destination of the push itself — merged, rebased, cherry-picked, doesn't matter, only where it's headed — so protection doesn't quietly evaporate the moment you (or an agent working on your behalf) does the merge-and-push in one motion without a real pause in between.
23
+
20
24
  No dashboard. No config file to argue with. No runtime dependencies — it's shell scripts wearing a [husky](https://typicode.github.io/husky/) trenchcoat.
21
25
 
22
26
  ## Install
@@ -29,7 +33,7 @@ That's the whole install, per project. It will, in order:
29
33
 
30
34
  - add `husky` as a devDependency (if you don't have it)
31
35
  - set `"scripts.prepare": "husky"` in `package.json`
32
- - drop `.husky/pre-commit` into your repo
36
+ - drop `.husky/pre-commit` and `.husky/pre-push` into your repo
33
37
 
34
38
  ### Or install it once, everywhere
35
39
 
@@ -89,7 +93,8 @@ Same secret scan, same AI review, run against the PR's diff against its base bra
89
93
  This is a guardrail, not a cage. Bad day, emergency hotfix, you know exactly what you're doing:
90
94
 
91
95
  ```bash
92
- git commit --no-verify
96
+ git commit --no-verify # skip the secret scan / AI review / auto-branch for one commit
97
+ git push --no-verify # skip the protected-branch push check for one push
93
98
  ```
94
99
 
95
100
  No questions asked. No shame either — that's what it's there for.
package/bin/cli.js CHANGED
@@ -39,7 +39,7 @@ function parseArgs(argv) {
39
39
  }
40
40
 
41
41
  function printHelp() {
42
- console.log(`no-yolo-commits — an AI pre-commit reviewer + "no direct commits to main" guard
42
+ console.log(`no-yolo-commits — an AI pre-commit reviewer + "no direct commits or pushes to main" guard
43
43
 
44
44
  Usage:
45
45
  npx no-yolo-commits init [options] per-project install (via husky)
@@ -57,7 +57,7 @@ Options:
57
57
  reviewer, e.g. --stack="Next.js + TypeScript + Postgres"
58
58
  (default: "TypeScript")
59
59
  --protect=a,b,c Comma-separated branch names that block direct commits
60
- (default: "main,master")
60
+ *and* pushes (default: "main,master")
61
61
  --model=NAME Pass --model NAME through to every \`claude\` invocation
62
62
  (cost/speed control — e.g. --model=haiku)
63
63
  --global, -g Install into git's global hook template
@@ -108,6 +108,11 @@ function buildHookScript(args, defaultPrefixSource) {
108
108
  return { hookScript, prefix, protectedBranches };
109
109
  }
110
110
 
111
+ function buildPrePushScript(protectedBranches) {
112
+ const templatePath = path.join(__dirname, '..', 'templates', 'pre-push.sh');
113
+ return fs.readFileSync(templatePath, 'utf8').replace(/__PROTECTED__/g, protectedBranches);
114
+ }
115
+
111
116
  function run(cmd, cmdArgs) {
112
117
  console.log(` $ ${cmd} ${cmdArgs.join(' ')}`);
113
118
  const result = spawnSync(cmd, cmdArgs, { cwd: CWD, stdio: 'inherit' });
@@ -128,9 +133,11 @@ function initProject(args) {
128
133
 
129
134
  const huskyDir = path.join(CWD, '.husky');
130
135
  const hookPath = path.join(huskyDir, 'pre-commit');
136
+ const pushHookPath = path.join(huskyDir, 'pre-push');
131
137
 
132
- if (fs.existsSync(hookPath) && !args.force) {
133
- fail(`${hookPath} already exists — pass --force to overwrite it.`);
138
+ if (!args.force) {
139
+ if (fs.existsSync(hookPath)) fail(`${hookPath} already exists — pass --force to overwrite it.`);
140
+ if (fs.existsSync(pushHookPath)) fail(`${pushHookPath} already exists — pass --force to overwrite it.`);
134
141
  }
135
142
 
136
143
  const hasHusky = (pkg.devDependencies && pkg.devDependencies.husky) || (pkg.dependencies && pkg.dependencies.husky);
@@ -153,12 +160,16 @@ function initProject(args) {
153
160
  run('npx', ['husky']);
154
161
 
155
162
  const { hookScript, prefix, protectedBranches } = buildHookScript(args, freshPkg.name);
163
+ const pushHookScript = buildPrePushScript(protectedBranches);
156
164
 
157
165
  fs.mkdirSync(huskyDir, { recursive: true });
158
166
  fs.writeFileSync(hookPath, hookScript);
159
167
  fs.chmodSync(hookPath, 0o755);
168
+ fs.writeFileSync(pushHookPath, pushHookScript);
169
+ fs.chmodSync(pushHookPath, 0o755);
160
170
 
161
171
  console.log(`✓ wrote ${path.relative(CWD, hookPath)}`);
172
+ console.log(`✓ wrote ${path.relative(CWD, pushHookPath)}`);
162
173
  printSummary(prefix, protectedBranches);
163
174
  }
164
175
 
@@ -171,17 +182,23 @@ function initGlobal(args) {
171
182
 
172
183
  const hooksDir = path.join(templateDir, 'hooks');
173
184
  const hookPath = path.join(hooksDir, 'pre-commit');
185
+ const pushHookPath = path.join(hooksDir, 'pre-push');
174
186
 
175
- if (fs.existsSync(hookPath) && !args.force) {
176
- fail(`${hookPath} already exists — pass --force to overwrite it.`);
187
+ if (!args.force) {
188
+ if (fs.existsSync(hookPath)) fail(`${hookPath} already exists — pass --force to overwrite it.`);
189
+ if (fs.existsSync(pushHookPath)) fail(`${pushHookPath} already exists — pass --force to overwrite it.`);
177
190
  }
178
191
 
179
192
  const { hookScript, prefix, protectedBranches } = buildHookScript(args, null);
193
+ const pushHookScript = buildPrePushScript(protectedBranches);
180
194
 
181
195
  fs.mkdirSync(hooksDir, { recursive: true });
182
196
  fs.writeFileSync(hookPath, hookScript);
183
197
  fs.chmodSync(hookPath, 0o755);
198
+ fs.writeFileSync(pushHookPath, pushHookScript);
199
+ fs.chmodSync(pushHookPath, 0o755);
184
200
  console.log(`✓ wrote ${hookPath}`);
201
+ console.log(`✓ wrote ${pushHookPath}`);
185
202
 
186
203
  run('git', ['config', '--global', 'init.templateDir', templateDir]);
187
204
 
@@ -201,7 +218,12 @@ function printSummary(prefix, protectedBranches) {
201
218
  console.log(' - AI review of staged changes (needs `claude` on PATH) — blocks only on high-confidence issues');
202
219
  console.log(` - direct commits to [${protectedBranches}] auto-branch instead, as ${prefix}-<timestamp>-<slug>`);
203
220
  console.log('');
221
+ console.log('And on `git push`:');
222
+ console.log(` - pushing to [${protectedBranches}] is refused outright — merged, rebased, whatever got`);
223
+ console.log(' a commit there locally doesn\'t matter, only where the push is headed');
224
+ console.log('');
204
225
  console.log('Bypass for one commit: git commit --no-verify');
226
+ console.log('Bypass for one push: git push --no-verify');
205
227
  }
206
228
 
207
229
  function init(args) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "no-yolo-commits",
3
- "version": "1.2.1",
4
- "description": "AI pre-commit review (via the claude CLI) + a guard that auto-branches you off main/master instead of letting a direct commit land there.",
3
+ "version": "1.3.0",
4
+ "description": "AI pre-commit review (via the claude CLI) + a guard that auto-branches you off main/master instead of letting a direct commit land there, and a pre-push hook that refuses any push to main/master outright regardless of how it got there locally.",
5
5
  "keywords": [
6
6
  "husky",
7
7
  "git-hooks",
@@ -21,7 +21,7 @@
21
21
  "url": "https://github.com/ataztech910/no-yolo-commits/issues"
22
22
  },
23
23
  "bin": {
24
- "no-yolo-commits": "./bin/cli.js"
24
+ "no-yolo-commits": "bin/cli.js"
25
25
  },
26
26
  "files": [
27
27
  "bin",
@@ -0,0 +1,35 @@
1
+ #!/usr/bin/env sh
2
+
3
+ # --- Refuse pushes that land on a protected branch ---
4
+ # git feeds this hook one line per ref being pushed on stdin:
5
+ # <local ref> <local sha1> <remote ref> <remote sha1>
6
+ # The auto-branch step in pre-commit only pauses a direct commit to main/master — it says nothing
7
+ # about what happens next. Fast-forward merge that branch locally and push, and the commit lands
8
+ # on the protected branch exactly as if pre-commit had never stepped in. This hook closes that gap
9
+ # by checking the actual destination of the push itself, independent of how the commit got there
10
+ # locally (direct commit that somehow slipped through, merge, rebase, whatever) — merged or not
11
+ # doesn't matter, only where it's headed.
12
+ #
13
+ # Deliberately blocks unconditionally rather than trying to special-case "but this one was already
14
+ # reviewed" — that judgment call is exactly what a human decides by reaching for --no-verify, not
15
+ # something a hook can infer from the ref list.
16
+ while read -r local_ref local_sha remote_ref remote_sha; do
17
+ [ -z "$remote_ref" ] && continue
18
+ remote_branch="${remote_ref#refs/heads/}"
19
+ case " __PROTECTED__ " in
20
+ *" $remote_branch "*)
21
+ echo ""
22
+ echo "⛔ Push to '$remote_branch' blocked."
23
+ echo " This repo doesn't push straight to a protected branch as a side effect of"
24
+ echo " merging or committing locally — that has to be a deliberate, visible step."
25
+ echo ""
26
+ echo " Push your branch instead (git push origin \$(git branch --show-current)) and"
27
+ echo " open a PR, or if you really do mean to push here directly:"
28
+ echo " git push --no-verify"
29
+ echo ""
30
+ exit 1
31
+ ;;
32
+ esac
33
+ done
34
+
35
+ exit 0