@rmyndharis/aimhooman 0.1.2 → 0.1.3

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.
@@ -5,7 +5,7 @@ This repository uses aimhooman to keep AI tooling artifacts out of Git history.
5
5
  Its policy:
6
6
 
7
7
  - Never stage or commit local AI session/state files. Examples include
8
- `.claude.json`, `.claude/.credentials.json`, `.claude/session*.json`,
8
+ `.claude.json`, `.claude/session*.json`,
9
9
  `.claude/history*`, `.claude/todos/*`, `.claude/shell-snapshots/*`,
10
10
  `.claude/statsig/*`, `.claude/projects/*`, `.claude/logs/*`,
11
11
  `.codex/sessions/*`, `.codex/history*`, `.codex/log/*`, `.codex/logs/*`,
@@ -15,8 +15,10 @@ Its policy:
15
15
  exhaustive; the packaged `rules/paths.json` is the detection source of truth.
16
16
  If one is staged, unstage it and keep it out of Git instead.
17
17
  - Never commit secrets: a real `.env` (not `.env.example`), private keys
18
- (`id_rsa` and files containing a private-key header), `.aws/credentials`, or
19
- service-account keys. Public certificates are allowed.
18
+ (`id_rsa` and files containing a private-key header), `.aws/credentials`,
19
+ `.claude/.credentials.json`, service-account keys, or a provider API key
20
+ (GitHub, GitLab, npm, Slack, Anthropic, OpenAI, Google, Stripe, Hugging Face,
21
+ SendGrid). Public certificates are allowed.
20
22
  - Never add AI attribution to commit messages: no `Co-authored-by` trailer naming
21
23
  an AI (Claude, Copilot, Codex), no "Generated with/by <AI>" lines, no AI-service
22
24
  noreply emails. A commit message reads as if a human wrote it.
@@ -9,7 +9,7 @@
9
9
  "name": "aimhooman",
10
10
  "source": "./",
11
11
  "description": "Keeps AI session files, secrets, and unwanted attribution out at the agent and Git boundary.",
12
- "version": "0.1.2",
12
+ "version": "0.1.3",
13
13
  "license": "MIT",
14
14
  "keywords": [
15
15
  "git",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aimhooman",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "AI works. Hoomans ship. Keeps AI session files, secrets, and unwanted attribution out at the agent and Git boundary.",
5
5
  "author": {
6
6
  "name": "aimhooman"
@@ -5,7 +5,7 @@ This repository uses aimhooman to keep AI tooling artifacts out of Git history.
5
5
  Its policy:
6
6
 
7
7
  - Never stage or commit local AI session/state files. Examples include
8
- `.claude.json`, `.claude/.credentials.json`, `.claude/session*.json`,
8
+ `.claude.json`, `.claude/session*.json`,
9
9
  `.claude/history*`, `.claude/todos/*`, `.claude/shell-snapshots/*`,
10
10
  `.claude/statsig/*`, `.claude/projects/*`, `.claude/logs/*`,
11
11
  `.codex/sessions/*`, `.codex/history*`, `.codex/log/*`, `.codex/logs/*`,
@@ -15,8 +15,10 @@ Its policy:
15
15
  exhaustive; the packaged `rules/paths.json` is the detection source of truth.
16
16
  If one is staged, unstage it and keep it out of Git instead.
17
17
  - Never commit secrets: a real `.env` (not `.env.example`), private keys
18
- (`id_rsa` and files containing a private-key header), `.aws/credentials`, or
19
- service-account keys. Public certificates are allowed.
18
+ (`id_rsa` and files containing a private-key header), `.aws/credentials`,
19
+ `.claude/.credentials.json`, service-account keys, or a provider API key
20
+ (GitHub, GitLab, npm, Slack, Anthropic, OpenAI, Google, Stripe, Hugging Face,
21
+ SendGrid). Public certificates are allowed.
20
22
  - Never add AI attribution to commit messages: no `Co-authored-by` trailer naming
21
23
  an AI (Claude, Copilot, Codex), no "Generated with/by <AI>" lines, no AI-service
22
24
  noreply emails. A commit message reads as if a human wrote it.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aimhooman",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "AI works. Hoomans ship. Keeps AI session files, secrets, and unwanted attribution out at the agent and Git boundary.",
5
5
  "author": {
6
6
  "name": "aimhooman maintainers",
@@ -11,7 +11,7 @@ This repository uses aimhooman to keep AI tooling artifacts out of Git history.
11
11
  Its policy:
12
12
 
13
13
  - Never stage or commit local AI session/state files. Examples include
14
- `.claude.json`, `.claude/.credentials.json`, `.claude/session*.json`,
14
+ `.claude.json`, `.claude/session*.json`,
15
15
  `.claude/history*`, `.claude/todos/*`, `.claude/shell-snapshots/*`,
16
16
  `.claude/statsig/*`, `.claude/projects/*`, `.claude/logs/*`,
17
17
  `.codex/sessions/*`, `.codex/history*`, `.codex/log/*`, `.codex/logs/*`,
@@ -21,8 +21,10 @@ Its policy:
21
21
  exhaustive; the packaged `rules/paths.json` is the detection source of truth.
22
22
  If one is staged, unstage it and keep it out of Git instead.
23
23
  - Never commit secrets: a real `.env` (not `.env.example`), private keys
24
- (`id_rsa` and files containing a private-key header), `.aws/credentials`, or
25
- service-account keys. Public certificates are allowed.
24
+ (`id_rsa` and files containing a private-key header), `.aws/credentials`,
25
+ `.claude/.credentials.json`, service-account keys, or a provider API key
26
+ (GitHub, GitLab, npm, Slack, Anthropic, OpenAI, Google, Stripe, Hugging Face,
27
+ SendGrid). Public certificates are allowed.
26
28
  - Never add AI attribution to commit messages: no `Co-authored-by` trailer naming
27
29
  an AI (Claude, Copilot, Codex), no "Generated with/by <AI>" lines, no AI-service
28
30
  noreply emails. A commit message reads as if a human wrote it.
@@ -5,7 +5,7 @@ This repository uses aimhooman to keep AI tooling artifacts out of Git history.
5
5
  Its policy:
6
6
 
7
7
  - Never stage or commit local AI session/state files. Examples include
8
- `.claude.json`, `.claude/.credentials.json`, `.claude/session*.json`,
8
+ `.claude.json`, `.claude/session*.json`,
9
9
  `.claude/history*`, `.claude/todos/*`, `.claude/shell-snapshots/*`,
10
10
  `.claude/statsig/*`, `.claude/projects/*`, `.claude/logs/*`,
11
11
  `.codex/sessions/*`, `.codex/history*`, `.codex/log/*`, `.codex/logs/*`,
@@ -15,8 +15,10 @@ Its policy:
15
15
  exhaustive; the packaged `rules/paths.json` is the detection source of truth.
16
16
  If one is staged, unstage it and keep it out of Git instead.
17
17
  - Never commit secrets: a real `.env` (not `.env.example`), private keys
18
- (`id_rsa` and files containing a private-key header), `.aws/credentials`, or
19
- service-account keys. Public certificates are allowed.
18
+ (`id_rsa` and files containing a private-key header), `.aws/credentials`,
19
+ `.claude/.credentials.json`, service-account keys, or a provider API key
20
+ (GitHub, GitLab, npm, Slack, Anthropic, OpenAI, Google, Stripe, Hugging Face,
21
+ SendGrid). Public certificates are allowed.
20
22
  - Never add AI attribution to commit messages: no `Co-authored-by` trailer naming
21
23
  an AI (Claude, Copilot, Codex), no "Generated with/by <AI>" lines, no AI-service
22
24
  noreply emails. A commit message reads as if a human wrote it.
@@ -5,7 +5,7 @@ This repository uses aimhooman to keep AI tooling artifacts out of Git history.
5
5
  Its policy:
6
6
 
7
7
  - Never stage or commit local AI session/state files. Examples include
8
- `.claude.json`, `.claude/.credentials.json`, `.claude/session*.json`,
8
+ `.claude.json`, `.claude/session*.json`,
9
9
  `.claude/history*`, `.claude/todos/*`, `.claude/shell-snapshots/*`,
10
10
  `.claude/statsig/*`, `.claude/projects/*`, `.claude/logs/*`,
11
11
  `.codex/sessions/*`, `.codex/history*`, `.codex/log/*`, `.codex/logs/*`,
@@ -15,8 +15,10 @@ Its policy:
15
15
  exhaustive; the packaged `rules/paths.json` is the detection source of truth.
16
16
  If one is staged, unstage it and keep it out of Git instead.
17
17
  - Never commit secrets: a real `.env` (not `.env.example`), private keys
18
- (`id_rsa` and files containing a private-key header), `.aws/credentials`, or
19
- service-account keys. Public certificates are allowed.
18
+ (`id_rsa` and files containing a private-key header), `.aws/credentials`,
19
+ `.claude/.credentials.json`, service-account keys, or a provider API key
20
+ (GitHub, GitLab, npm, Slack, Anthropic, OpenAI, Google, Stripe, Hugging Face,
21
+ SendGrid). Public certificates are allowed.
20
22
  - Never add AI attribution to commit messages: no `Co-authored-by` trailer naming
21
23
  an AI (Claude, Copilot, Codex), no "Generated with/by <AI>" lines, no AI-service
22
24
  noreply emails. A commit message reads as if a human wrote it.
@@ -9,7 +9,7 @@ This repository uses aimhooman to keep AI tooling artifacts out of Git history.
9
9
  Its policy:
10
10
 
11
11
  - Never stage or commit local AI session/state files. Examples include
12
- `.claude.json`, `.claude/.credentials.json`, `.claude/session*.json`,
12
+ `.claude.json`, `.claude/session*.json`,
13
13
  `.claude/history*`, `.claude/todos/*`, `.claude/shell-snapshots/*`,
14
14
  `.claude/statsig/*`, `.claude/projects/*`, `.claude/logs/*`,
15
15
  `.codex/sessions/*`, `.codex/history*`, `.codex/log/*`, `.codex/logs/*`,
@@ -19,8 +19,10 @@ Its policy:
19
19
  exhaustive; the packaged `rules/paths.json` is the detection source of truth.
20
20
  If one is staged, unstage it and keep it out of Git instead.
21
21
  - Never commit secrets: a real `.env` (not `.env.example`), private keys
22
- (`id_rsa` and files containing a private-key header), `.aws/credentials`, or
23
- service-account keys. Public certificates are allowed.
22
+ (`id_rsa` and files containing a private-key header), `.aws/credentials`,
23
+ `.claude/.credentials.json`, service-account keys, or a provider API key
24
+ (GitHub, GitLab, npm, Slack, Anthropic, OpenAI, Google, Stripe, Hugging Face,
25
+ SendGrid). Public certificates are allowed.
24
26
  - Never add AI attribution to commit messages: no `Co-authored-by` trailer naming
25
27
  an AI (Claude, Copilot, Codex), no "Generated with/by <AI>" lines, no AI-service
26
28
  noreply emails. A commit message reads as if a human wrote it.
package/AGENTS.md CHANGED
@@ -5,7 +5,7 @@ This repository uses aimhooman to keep AI tooling artifacts out of Git history.
5
5
  Its policy:
6
6
 
7
7
  - Never stage or commit local AI session/state files. Examples include
8
- `.claude.json`, `.claude/.credentials.json`, `.claude/session*.json`,
8
+ `.claude.json`, `.claude/session*.json`,
9
9
  `.claude/history*`, `.claude/todos/*`, `.claude/shell-snapshots/*`,
10
10
  `.claude/statsig/*`, `.claude/projects/*`, `.claude/logs/*`,
11
11
  `.codex/sessions/*`, `.codex/history*`, `.codex/log/*`, `.codex/logs/*`,
@@ -15,8 +15,10 @@ Its policy:
15
15
  exhaustive; the packaged `rules/paths.json` is the detection source of truth.
16
16
  If one is staged, unstage it and keep it out of Git instead.
17
17
  - Never commit secrets: a real `.env` (not `.env.example`), private keys
18
- (`id_rsa` and files containing a private-key header), `.aws/credentials`, or
19
- service-account keys. Public certificates are allowed.
18
+ (`id_rsa` and files containing a private-key header), `.aws/credentials`,
19
+ `.claude/.credentials.json`, service-account keys, or a provider API key
20
+ (GitHub, GitLab, npm, Slack, Anthropic, OpenAI, Google, Stripe, Hugging Face,
21
+ SendGrid). Public certificates are allowed.
20
22
  - Never add AI attribution to commit messages: no `Co-authored-by` trailer naming
21
23
  an AI (Claude, Copilot, Codex), no "Generated with/by <AI>" lines, no AI-service
22
24
  noreply emails. A commit message reads as if a human wrote it.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,36 @@ All notable changes to this project are documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.1.3] - 2026-07-18
9
+
10
+ ### Fixed
11
+
12
+ - Repair that empties the index no longer mints an empty commit. Stage only a `.env`,
13
+ run `git commit -m "add config"`, and the pre-commit repair unstages it and prints
14
+ "the commit will be empty" — then Git made that empty commit anyway. Git refuses a
15
+ commit with nothing staged, but repair runs after Git has already decided to proceed,
16
+ so carrying on created a commit Git would never have made and left a junk commit to
17
+ `git reset --hard`. Pre-commit now exits 10 when the repair empties the index, so Git
18
+ stops. Nothing else staged means nothing to commit.
19
+ - `build && git add . && git commit` is no longer denied. Each half already passed on its
20
+ own; only the pair was refused. The commit-time-staging deny asked the wide bypass
21
+ predicate — the one that decides whether the staged-content backstop reads the blobs —
22
+ instead of the narrow question of whether anything will scan the commit. An unmodelled
23
+ prefix is not a bypass, and pre-commit still scans the real index at commit time. The
24
+ deny now asks that narrow question. A prefix that reaches the hooks, `--no-verify`, and
25
+ a real `core.hooksPath` override all keep the deny, where the staged files really would
26
+ go unscanned. The refusal message no longer names "--no-verify or shell indirection"
27
+ when neither is present.
28
+ - The `PreToolUse` guard stops refusing more everyday read-only pipelines. Building on the
29
+ 0.1.1 filter allowance, a read-only Git subcommand is now an allowed pipe source, so
30
+ `git log | head`, `git status | grep modified`, `git diff | cat`, and
31
+ `cd repo && git log | head` run. Build and test toolchains (`npm`, `cargo`, `make`,
32
+ `jest`, `eslint`, `tsc`, `pytest`) are allowed as a source. The listing forms of
33
+ branch/tag/remote/stash/notes run (`git branch | grep`, `git remote -v | grep origin`),
34
+ while their mutating forms (`git branch -D`, `git tag v1`) stay denied. Git as a pipe
35
+ sink, `git commit --no-verify`, and `git push` still deny; the reference-transaction and
36
+ pre-commit hooks remain the boundary.
37
+
8
38
  ## [0.1.2] - 2026-07-17
9
39
 
10
40
  ### Removed
package/GEMINI.md CHANGED
@@ -5,7 +5,7 @@ This repository uses aimhooman to keep AI tooling artifacts out of Git history.
5
5
  Its policy:
6
6
 
7
7
  - Never stage or commit local AI session/state files. Examples include
8
- `.claude.json`, `.claude/.credentials.json`, `.claude/session*.json`,
8
+ `.claude.json`, `.claude/session*.json`,
9
9
  `.claude/history*`, `.claude/todos/*`, `.claude/shell-snapshots/*`,
10
10
  `.claude/statsig/*`, `.claude/projects/*`, `.claude/logs/*`,
11
11
  `.codex/sessions/*`, `.codex/history*`, `.codex/log/*`, `.codex/logs/*`,
@@ -15,8 +15,10 @@ Its policy:
15
15
  exhaustive; the packaged `rules/paths.json` is the detection source of truth.
16
16
  If one is staged, unstage it and keep it out of Git instead.
17
17
  - Never commit secrets: a real `.env` (not `.env.example`), private keys
18
- (`id_rsa` and files containing a private-key header), `.aws/credentials`, or
19
- service-account keys. Public certificates are allowed.
18
+ (`id_rsa` and files containing a private-key header), `.aws/credentials`,
19
+ `.claude/.credentials.json`, service-account keys, or a provider API key
20
+ (GitHub, GitLab, npm, Slack, Anthropic, OpenAI, Google, Stripe, Hugging Face,
21
+ SendGrid). Public certificates are allowed.
20
22
  - Never add AI attribution to commit messages: no `Co-authored-by` trailer naming
21
23
  an AI (Claude, Copilot, Codex), no "Generated with/by <AI>" lines, no AI-service
22
24
  noreply emails. A commit message reads as if a human wrote it.
package/README.md CHANGED
@@ -9,7 +9,7 @@
9
9
 
10
10
  <p align="center">
11
11
  <img src="https://img.shields.io/github/actions/workflow/status/rmyndharis/aimhooman/test.yml?branch=main&label=CI" alt="CI">
12
- <img src="https://img.shields.io/badge/version-v0.1.2-blue" alt="v0.1.2">
12
+ <img src="https://img.shields.io/badge/version-v0.1.3-blue" alt="v0.1.3">
13
13
  <img src="https://img.shields.io/badge/node-%E2%89%A522.8-339933?logo=node.js&logoColor=white" alt="Node 22.8+">
14
14
  <img src="https://img.shields.io/badge/dependencies-0-brightgreen" alt="Zero dependencies">
15
15
  <img src="https://img.shields.io/badge/license-MIT-111111" alt="MIT">
@@ -154,8 +154,11 @@ ignores `.git/hooks`. Repository `init` installs and chains predecessors only wh
154
154
  that directory is absent or is proven to be owned by the repository: inside it and
155
155
  not tracked by Git. It refuses to modify a global, shared, external, or tracked
156
156
  hook directory, because a dispatcher committed from one machine names paths that
157
- exist only on that machine. In that case, integrate
158
- aimhooman into the existing hook manager or remove the override before retrying.
157
+ exist only on that machine. Those repositories are not guarded, and there is no
158
+ way to guard them today. Calling `aimhooman precommit` from an existing hook
159
+ manager runs the check but registers no managed guard, so the agent hook still
160
+ refuses the commit. Remove the override before retrying, or accept that the
161
+ repository is unguarded and do not run `init` there.
159
162
 
160
163
  Repository `init` installs `pre-commit`, `pre-merge-commit`, `commit-msg`, and
161
164
  `reference-transaction`, and preserves an existing hook as a predecessor. For
@@ -238,7 +241,8 @@ files automatically.
238
241
  `.agent/`. [`rules/paths.json`](rules/paths.json) is the complete catalog.
239
242
  - **Secrets**: a real `.env` (not `.env.example`), private-key content,
240
243
  `.aws/credentials`, service-account private keys, recognized AWS secret/session
241
- assignments, and provider token prefixes for GitHub, GitLab, npm, and Slack.
244
+ assignments, and provider token prefixes for GitHub, GitLab, npm, Slack,
245
+ Anthropic, OpenAI, Google, Stripe, Hugging Face, and SendGrid.
242
246
  Public certificates are allowed.
243
247
  - **AI attribution** in commit messages: known AI `Co-authored-by:` identities,
244
248
  exact "Generated with/by ..." lines, and AI-service noreply attribution trailers.
@@ -410,9 +414,13 @@ that would open an editor, and commits with an active foreign `prepare-commit-ms
410
414
  It still cannot prove that every program already selected by local Git config is safe.
411
415
  Commands assembled from files, encoded data, or network input may also be invisible to
412
416
  its non-executing shell parser (POSIX shells — bash, sh, zsh, dash, ksh — and
413
- `git.exe`; command nesting, pipelines, background jobs, and non-POSIX executors
414
- such as PowerShell or fish are classified uncertain and, on `strict`, denied
415
- with a retry instruction). Repository selection written in non-POSIX shell syntax
417
+ `git.exe`). Everyday read-only pipelines run, because a read-only source cannot hide or
418
+ feed a commit: `git log | head`, `git status | grep modified`, `git diff | cat`,
419
+ `git branch | grep`, `npm test | tail`, `cargo build 2>&1 | grep error`, and
420
+ `cd repo && git log | head` all pass. What stays uncertain and, on `strict`, is denied
421
+ with a retry instruction: a git command as a pipe *sink* (`cat patch | git apply`),
422
+ pipe-to-shell (`curl x | bash`), command nesting, background jobs, and non-POSIX
423
+ executors such as PowerShell or fish. Repository selection written in non-POSIX shell syntax
416
424
  is denied before policy lookup on every profile. Guarded Git changes, including
417
425
  `add`, commit, and ref updates, also reject shell-expanded targets, any leading-tilde
418
426
  target, POSIX targets beginning with exactly `//`, and an explicit split
package/bin/aimhooman.mjs CHANGED
@@ -59,6 +59,42 @@ function tone() {
59
59
  return process.env.AIMHOOMAN_TONE === 'professional' || process.env.CI ? 'professional' : 'playful';
60
60
  }
61
61
 
62
+ // The per-scan budget sizes the `cat-file --batch` read buffer, so a value that
63
+ // travelled with a clone would let a hostile repository drive the verifier out
64
+ // of memory instead of being rejected by it. These stay in the environment for
65
+ // that reason, never in .aimhooman.json, and a raise is capped for the same one.
66
+ // Lowering needs no ceiling: a smaller budget only skips more, and a skip is
67
+ // already an incomplete scan, which fails closed at 31.
68
+ const MAX_SCAN_LIMIT_BYTES = 1 << 30;
69
+
70
+ // Crossing a scan budget marks the scan incomplete on every profile, and an
71
+ // incomplete scan is a ref update the reference-transaction hook refuses — the
72
+ // one boundary --no-verify does not skip. A repository tracking a lockfile, an
73
+ // image or a vendored bundle over the default therefore could not commit at all,
74
+ // and "reduce the target or limits and retry" named something no flag, schema
75
+ // field or config key could do. This is that knob.
76
+ function scanLimits() {
77
+ const limits = {};
78
+ for (const [key, name] of [
79
+ ['maxFileBytes', 'AIMHOOMAN_MAX_FILE_BYTES'],
80
+ ['maxTotalBytes', 'AIMHOOMAN_MAX_TOTAL_BYTES'],
81
+ ]) {
82
+ const raw = process.env[name];
83
+ if (raw === undefined || raw === '') continue;
84
+ if (!/^\d+$/.test(raw)) {
85
+ throw new ArgumentError(`${name} must be a whole number of bytes; got "${visible(raw)}"`);
86
+ }
87
+ const value = Number(raw);
88
+ if (value < 1 || value > MAX_SCAN_LIMIT_BYTES) {
89
+ throw new ArgumentError(
90
+ `${name} must be between 1 and ${MAX_SCAN_LIMIT_BYTES} bytes; got ${raw}`
91
+ );
92
+ }
93
+ limits[key] = value;
94
+ }
95
+ return limits;
96
+ }
97
+
62
98
  function configuredEngine(profile, repo) {
63
99
  const { engine, errors } = newEngineWithDiagnostics(profile, repo?.stateDir);
64
100
  if (errors.length && profile === 'strict') {
@@ -166,10 +202,16 @@ function incompleteMessage(scan) {
166
202
  .join(', ');
167
203
  // Every other reason is a size or budget the caller can shrink. A pack that
168
204
  // will not compile is not, and the warning above already names the file and
169
- // the error, so point at that instead of misdirecting to the limits.
205
+ // the error, so point at that instead of misdirecting to the limits. When a
206
+ // byte budget is what stopped the scan, name the budget: the caller whose own
207
+ // tree outgrew it needs to raise one, and "reduce the limits" sends them the
208
+ // wrong way down a road they cannot leave.
209
+ const budgeted = reasons['size-limit'] || reasons['total-byte-limit'];
170
210
  const hint = reasons['local-pack-error']
171
211
  ? 'fix the reported rule pack and retry'
172
- : 'reduce the target or limits and retry';
212
+ : budgeted
213
+ ? 'reduce the target, or raise AIMHOOMAN_MAX_FILE_BYTES / AIMHOOMAN_MAX_TOTAL_BYTES, and retry'
214
+ : 'reduce the target or limits and retry';
173
215
  return `aimhooman: scan incomplete${skipped ? ` (${skipped})` : ''}; ${hint}\n`;
174
216
  }
175
217
 
@@ -256,9 +298,10 @@ function cmdPrecommit(args) {
256
298
  parseNoArguments(args);
257
299
  const repo = tryRepo();
258
300
  if (!repo) { console.error('aimhooman: not a git repository'); return 30; }
301
+ const limits = scanLimits();
259
302
  let scan;
260
303
  try {
261
- scan = scanGitTarget(repo, { kind: 'staged' });
304
+ scan = scanGitTarget(repo, { kind: 'staged', limits });
262
305
  } catch (e) {
263
306
  console.error(`aimhooman: cannot scan staged content: ${e.message}`);
264
307
  return expectedErrorCode(e);
@@ -284,6 +327,7 @@ function cmdPrecommit(args) {
284
327
  return 10;
285
328
  }
286
329
  const paths = [...new Set(blocks.map((f) => f.path).filter(Boolean))];
330
+ let emptied = false;
287
331
  try {
288
332
  const unstageTargets = new Set(paths);
289
333
  for (const finding of blocks) {
@@ -292,15 +336,15 @@ function cmdPrecommit(args) {
292
336
  }
293
337
  }
294
338
  for (const source of stagedRenameSources(repo, paths)) unstageTargets.add(source);
295
- // The empty-commit hint is derived from the staged paths captured before
296
- // repair, not from a second git read after `git restore --staged`. That
297
- // post-repair read followed an index write and could transiently report
298
- // the wrong state under heavy CI load, flaking the repair tests.
299
- // unstagePaths is atomic (it throws on failure), so when every staged
300
- // path is a repair target the index matches HEAD afterward. The capture
301
- // is best-effort and runs before the repair: if this read fails the
302
- // unstage still runs and the hint is omitted (empty stays false) rather
303
- // than skipping the repair or blocking the commit.
339
+ // Whether the repair empties the index is derived from the staged paths
340
+ // captured before repair, not from a second git read after
341
+ // `git restore --staged`. That post-repair read followed an index write
342
+ // and could transiently report the wrong state under heavy CI load,
343
+ // flaking the repair tests. unstagePaths is atomic (it throws on
344
+ // failure), so when every staged path is a repair target the index
345
+ // matches HEAD afterward. The capture is best-effort and runs before the
346
+ // repair: if this read fails the unstage still runs and the commit is
347
+ // left to proceed rather than blocked on a state we could not read.
304
348
  let stagedBefore;
305
349
  try {
306
350
  stagedBefore = stagedPaths(repo);
@@ -325,10 +369,10 @@ function cmdPrecommit(args) {
325
369
  if (!pending.length) break;
326
370
  unstagePaths(repo, pending);
327
371
  }
328
- const empty = stagedBefore !== null
372
+ emptied = stagedBefore !== null
329
373
  && stagedBefore.every((path) => unstageTargets.has(path));
330
374
  process.stderr.write(
331
- `aimhooman: unstaged ${paths.length} AI artifact(s) from this commit: ${paths.map(visible).join(', ')}${empty ? ' (nothing else staged the commit will be empty)' : ''}\n`
375
+ `aimhooman: unstaged ${paths.length} file(s) from this commit: ${paths.map(visible).join(', ')}${emptied ? ' nothing else was staged, so the commit is stopped rather than left empty' : ''}\n`
332
376
  );
333
377
  } catch (e) {
334
378
  process.stderr.write(
@@ -337,8 +381,20 @@ function cmdPrecommit(args) {
337
381
  );
338
382
  return 10;
339
383
  }
340
- if (reviews.length) process.stderr.write(human(reviews, tone()));
384
+ // Seven of the rules that can reach the summary above are secret rules, so it
385
+ // cannot name a cause without guessing wrong for a private key or an AWS
386
+ // credential — and it used to be the only thing printed for a block. Let the
387
+ // findings speak instead: human() already carries the rule id, the path, the
388
+ // reason and the remediation, and already redacts secret text. It labels each
389
+ // one BLOCK, which is the decision that unstaged the path, not a stopped
390
+ // commit; the summary above says what actually happened to them.
391
+ process.stderr.write(human([...blocks, ...reviews], tone()));
341
392
  if (!scan.complete) process.stderr.write(incompleteMessage(scan));
393
+ // Git refuses a commit with nothing staged; repair runs after git has already
394
+ // decided otherwise, so carrying on here mints the empty commit git would
395
+ // not. The request was to commit the artifact, not to stamp the history with
396
+ // its message and no content.
397
+ if (emptied) return 10;
342
398
  return scan.complete ? 0 : 31;
343
399
  }
344
400
 
@@ -380,12 +436,13 @@ function cmdCommitmsg(args) {
380
436
  }
381
437
  const text = messageBytes.toString('utf8');
382
438
  const validUtf8 = Buffer.from(text, 'utf8').equals(messageBytes);
439
+ const limits = scanLimits();
383
440
  let scan;
384
441
  let treeScan = null;
385
442
  try {
386
443
  const checked = againstWouldBeTree(() => ({
387
444
  message: scanMessage(repo, text, { target: 'staged' }),
388
- tree: options.tree ? scanGitTarget(repo, { kind: 'staged' }) : null,
445
+ tree: options.tree ? scanGitTarget(repo, { kind: 'staged', limits }) : null,
389
446
  }));
390
447
  scan = checked.message;
391
448
  treeScan = checked.tree;
@@ -461,18 +518,12 @@ function cmdRefcheck(args) {
461
518
  if (phase === 'committed' || phase === 'aborted') return 0;
462
519
 
463
520
  const repo = tryRepo();
464
- if (!repo) {
465
- // Global hooks also run in bare repositories. Bare repositories have no
466
- // worktree/index policy boundary and are intentionally unsupported, so
467
- // a global dispatcher must remain transparent there instead of making
468
- // every receive-pack or update-ref fail merely because --show-toplevel
469
- // is unavailable.
470
- if (currentRepositoryIsBare()) return 0;
471
- console.error('aimhooman: not a git repository');
472
- return 30;
473
- }
474
521
  if (phase === 'preparing') {
475
- return dispatchHooksChanged(repo, 'clean') ? 20 : 0;
522
+ // This phase never scans, and without a repository there is no
523
+ // dispatcher directory to inspect either. Git fires it while `git init`
524
+ // is still building the repository, so refusing here would only stop
525
+ // repository creation on behalf of a phase with nothing to say.
526
+ return repo && dispatchHooksChanged(repo, 'clean') ? 20 : 0;
476
527
  }
477
528
  let input;
478
529
  try { input = readFileSync(0, 'utf8'); }
@@ -499,6 +550,31 @@ function cmdRefcheck(args) {
499
550
  updates.push({ oldObjectId, newObjectId, ref });
500
551
  }
501
552
 
553
+ if (!repo) {
554
+ // Git writes the initial HEAD inside a reference transaction and fires
555
+ // this hook while the repository is still being built: GIT_DIR is
556
+ // exported but nothing can be queried yet, so every rev-parse fails,
557
+ // --is-bare-repository included. The bare carve-out below therefore
558
+ // cannot speak for `git init` or `git init --bare`, and reading the
559
+ // silence as "not a git repository" aborted the transaction and left a
560
+ // half-built .git behind that a second `git init` could not repair.
561
+ // That payload is `0000..0000 ref:refs/heads/<name> HEAD`, which the
562
+ // symref filter above already dropped, so no policy-relevant update
563
+ // survives and there is nothing to scan. The scope is "the payload
564
+ // carries nothing", never "git failed to answer": a transaction holding
565
+ // a real object still demands a repository, so no one can disable the
566
+ // last veto by arranging for openRepo to fail.
567
+ if (!updates.length) return 0;
568
+ // Global hooks also run in bare repositories. Bare repositories have no
569
+ // worktree/index policy boundary and are intentionally unsupported, so
570
+ // a global dispatcher must remain transparent there instead of making
571
+ // every receive-pack or update-ref fail merely because --show-toplevel
572
+ // is unavailable.
573
+ if (currentRepositoryIsBare()) return 0;
574
+ console.error('aimhooman: not a git repository');
575
+ return 30;
576
+ }
577
+
502
578
  let commits;
503
579
  try {
504
580
  const contextsByCommit = new Map();
@@ -533,6 +609,7 @@ function cmdRefcheck(args) {
533
609
  console.error(`aimhooman: cannot resolve proposed commits: ${error.message}`);
534
610
  return 30;
535
611
  }
612
+ const limits = scanLimits();
536
613
  for (const [revision, reviewContexts] of commits) {
537
614
  let scan;
538
615
  try {
@@ -541,6 +618,7 @@ function cmdRefcheck(args) {
541
618
  revision,
542
619
  reviewContexts,
543
620
  policyMigrationContexts: reviewContexts,
621
+ limits,
544
622
  });
545
623
  }
546
624
  catch (error) {
@@ -593,6 +671,7 @@ function cmdCheck(args) {
593
671
  return 30;
594
672
  }
595
673
  }
674
+ const limits = scanLimits();
596
675
  let scan;
597
676
  try {
598
677
  if (o.message && !o.staged && !o.tracked) {
@@ -606,6 +685,7 @@ function cmdCheck(args) {
606
685
  revision: o.commit,
607
686
  range: o.range,
608
687
  explicitProfile: o.profile,
688
+ limits,
609
689
  ...(o.message ? { messageText } : {}),
610
690
  });
611
691
  }
@@ -1537,7 +1617,18 @@ function cmdUninstall(args) {
1537
1617
  }
1538
1618
  return withLock(join(repo.commonDir, 'aimhooman-lifecycle.lock'), () => {
1539
1619
  const rep = uninstallHooks(repo);
1540
- removeExclude(repo.excludeFile);
1620
+ // The irreversible work is already done above, and the report is still
1621
+ // below. A throw from here unwound past all of it, so a damaged marker was
1622
+ // the only thing the user heard while four dispatchers kept guarding every
1623
+ // commit. Report the failure beside the removal report instead of in place
1624
+ // of it. Nothing is swallowed: this is also where the symlink and permission
1625
+ // guards surface, and their messages still reach the user and still exit 30.
1626
+ let excludeFailure = '';
1627
+ try {
1628
+ removeExclude(repo.excludeFile);
1629
+ } catch (error) {
1630
+ excludeFailure = `exclude block left in ${repo.excludeFile}: ${error.message}`;
1631
+ }
1541
1632
  // Trust the directory, not the report. Every refusal below leaves a working
1542
1633
  // dispatcher behind, and one printed under "uninstalled" reads as done.
1543
1634
  const remaining = remainingDispatchers(repo);
@@ -1552,6 +1643,7 @@ function cmdUninstall(args) {
1552
1643
  if (rep.restored.length) console.log(` hooks restored: ${rep.restored.join(', ')}`);
1553
1644
  for (const w of rep.warnings || []) console.log(` warning: ${w}`);
1554
1645
  for (const f of rep.failures || []) console.error(` failure: ${f}`);
1646
+ if (excludeFailure) console.error(` failure: ${excludeFailure}`);
1555
1647
  const unrestored = purge ? unrestoredChainedBackups(repo) : [];
1556
1648
  if (purge) {
1557
1649
  // Never wipe stateDir while a predecessor hook backup is still on disk:
@@ -1587,7 +1679,9 @@ function cmdUninstall(args) {
1587
1679
  console.log(' eligible non-bare repositories that inherit core.hooksPath are still guarded.');
1588
1680
  console.log(' run `aimhooman uninstall --global` to remove it.');
1589
1681
  }
1590
- return remaining.length || rep.failures?.length || unrestored.length ? 30 : 0;
1682
+ // The managed block is still in the exclude file and still ignoring paths, so
1683
+ // the uninstall is genuinely incomplete and 30 is the honest answer.
1684
+ return remaining.length || rep.failures?.length || unrestored.length || excludeFailure ? 30 : 0;
1591
1685
  }, LIFECYCLE_LOCK_OPTIONS);
1592
1686
  }
1593
1687
 
@@ -65,6 +65,30 @@ Git 2.54 also emits an earlier `preparing` reference-transaction callback. The
65
65
  hook checks dispatcher integrity there without scanning unresolved references,
66
66
  then keeps the full-scan veto at `prepared`, after Git has locked them.
67
67
 
68
+ ### Frictionless agent guard (the everyday commands run)
69
+
70
+ The layer-2 PreToolUse guard reports paths and refuses unprovable protected Git
71
+ mutations. It does not stand between a developer and the commands they run to read a
72
+ repository. A read-only Git subcommand piped to a filter is a source: `git log | head`,
73
+ `git status | grep modified`, `git diff | cat`, and the `cd repo && git log | head`
74
+ prefix produce stdout for the downstream filter and cannot hide or feed a commit, so
75
+ they run with no refusal. Build and test toolchains (`npm`, `cargo`, `make`, `jest`,
76
+ `eslint`, `tsc`, `pytest`, ...) are allowed as a source for the same reason. The listing
77
+ forms of branch/tag/remote/stash/notes move no ref and run; their mutating forms
78
+ (`git branch -D`, `git tag v1`) still reach the reference-transaction guard. A commit
79
+ made of already-safe halves runs as a whole: `build && git add . && git commit` is
80
+ allowed, because an unmodelled prefix is not a bypass and pre-commit still scans the real
81
+ index at commit time.
82
+
83
+ What stays refused is what can actually get past the scan. Git as a pipe sink
84
+ (`cat patch | git apply`) reads stdin, which can drive `apply`/`am`/`hash-object`.
85
+ Pipe-to-shell, subshells, `eval`, command substitution, and readers that execute their
86
+ own input stay opaque. `git commit --no-verify`, an explicit `core.hooksPath` override,
87
+ and `git push` refuse on the tiers that own them. The boundary that decides whether a
88
+ commit is scanned is the pre-commit and reference-transaction hook, never a denial of the
89
+ shell line that reads the repo. Refusing the read taught agents to drop the `&&` gate
90
+ rather than run the command on its own, which is the friction this tier exists to avoid.
91
+
68
92
  ### Components
69
93
 
70
94
  1. **Rule catalog** — `rules/paths.json`, `rules/attribution.json`,
@@ -118,6 +142,10 @@ means local rules only add restrictions and can never override a core block.
118
142
  transparent in unsupported bare repositories.
119
143
  - **HEAD-safe unstage.** `git restore --staged` needs HEAD; on a repository's
120
144
  initial commit it falls back to `git rm --cached --ignore-unmatch`.
145
+ - **Repair never mints an empty commit.** When the pre-commit repair unstages the last
146
+ staged path (stage only a `.env`, then `git commit`), it exits 10 so Git stops, the
147
+ same outcome as committing with nothing staged. Carrying on would create a commit Git
148
+ itself would have refused and leave the developer a junk commit to `git reset --hard`.
121
149
  - **Anchored attribution rules.** The AI-noreply rule is anchored to trailer
122
150
  lines (`*-by:`) so it never strips a prose line that merely mentions the email.
123
151
  - **`compliance` keeps disclosure.** AI-attribution rules resolve to `allow`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rmyndharis/aimhooman",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "AI works. Hoomans ship. Keep AI session files, secrets, and attribution out of your commits.",
5
5
  "homepage": "https://github.com/rmyndharis/aimhooman#readme",
6
6
  "repository": {