@rmyndharis/aimhooman 0.1.2 → 0.1.4

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.4",
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.4",
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.4",
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,55 @@ 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.4] - 2026-07-18
9
+
10
+ ### Fixed
11
+
12
+ - Uninstall no longer leaves aimhooman artifacts in `.git`. Every atomic write takes a
13
+ lock whose `<lock>.queue` directory was created but never removed — the release path
14
+ only unlinked its own candidate file — and `--purge-state` cleared just the state
15
+ directory. So `.git` kept the queue directories and the `COMMIT_EDITMSG.aimhooman-bak`
16
+ attribution backup while the command printed "state purged". Uninstall now sweeps this
17
+ residue once the lifecycle lock releases; `rmdirSync` removes only an empty directory,
18
+ so a concurrent contender's queue is never touched, and the lifecycle queue is removable
19
+ precisely because its own lock has already released. A plain uninstall keeps policy
20
+ state and drops the operational residue; `--purge-state` leaves no aimhooman fingerprints.
21
+ - A bare AWS access key ID is now detected. `secret.aws-key-content` fired only when an
22
+ `aws_secret_access_key` or `aws_session_token` name sat beside the value, so an
23
+ `AKIA…`/`ASIA…` access key ID committed on its own passed clean. The rule now matches the
24
+ access-key-ID prefixes directly — the fixed-prefix, highest-confidence AWS indicator. The
25
+ 16-character body and word boundaries keep short lookalikes such as `AKIA123` from matching.
26
+
27
+ ## [0.1.3] - 2026-07-18
28
+
29
+ ### Fixed
30
+
31
+ - Repair that empties the index no longer mints an empty commit. Stage only a `.env`,
32
+ run `git commit -m "add config"`, and the pre-commit repair unstages it and prints
33
+ "the commit will be empty" — then Git made that empty commit anyway. Git refuses a
34
+ commit with nothing staged, but repair runs after Git has already decided to proceed,
35
+ so carrying on created a commit Git would never have made and left a junk commit to
36
+ `git reset --hard`. Pre-commit now exits 10 when the repair empties the index, so Git
37
+ stops. Nothing else staged means nothing to commit.
38
+ - `build && git add . && git commit` is no longer denied. Each half already passed on its
39
+ own; only the pair was refused. The commit-time-staging deny asked the wide bypass
40
+ predicate — the one that decides whether the staged-content backstop reads the blobs —
41
+ instead of the narrow question of whether anything will scan the commit. An unmodelled
42
+ prefix is not a bypass, and pre-commit still scans the real index at commit time. The
43
+ deny now asks that narrow question. A prefix that reaches the hooks, `--no-verify`, and
44
+ a real `core.hooksPath` override all keep the deny, where the staged files really would
45
+ go unscanned. The refusal message no longer names "--no-verify or shell indirection"
46
+ when neither is present.
47
+ - The `PreToolUse` guard stops refusing more everyday read-only pipelines. Building on the
48
+ 0.1.1 filter allowance, a read-only Git subcommand is now an allowed pipe source, so
49
+ `git log | head`, `git status | grep modified`, `git diff | cat`, and
50
+ `cd repo && git log | head` run. Build and test toolchains (`npm`, `cargo`, `make`,
51
+ `jest`, `eslint`, `tsc`, `pytest`) are allowed as a source. The listing forms of
52
+ branch/tag/remote/stash/notes run (`git branch | grep`, `git remote -v | grep origin`),
53
+ while their mutating forms (`git branch -D`, `git tag v1`) stay denied. Git as a pipe
54
+ sink, `git commit --no-verify`, and `git push` still deny; the reference-transaction and
55
+ pre-commit hooks remain the boundary.
56
+
8
57
  ## [0.1.2] - 2026-07-17
9
58
 
10
59
  ### 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.4-blue" alt="v0.1.4">
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
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { execFileSync } from 'node:child_process';
3
- import { chmodSync, lstatSync, readFileSync, rmSync } from 'node:fs';
3
+ import { chmodSync, lstatSync, readFileSync, rmdirSync, rmSync } from 'node:fs';
4
4
  import { join } from 'node:path';
5
5
  import { fileURLToPath } from 'node:url';
6
6
  import { GIT_TIMEOUT_MS } from '../src/git-environment.mjs';
@@ -20,7 +20,7 @@ import {
20
20
  } from '../src/gitx.mjs';
21
21
  import { loadConfig, loadOverrides, loadProjectPolicy, normalizeOverrideTarget, saveConfig, saveOverrides } from '../src/state.mjs';
22
22
  import { applyExclude, inspectExclude, patternsForRules, removeExclude } from '../src/exclude.mjs';
23
- import { hookDiagnostics, installHooks, installGlobalHooks, uninstallGlobalHooks, globalHooksDir, installedHooks, remainingDispatchers, uninstallHooks, unrestoredChainedBackups } from '../src/githooks.mjs';
23
+ import { effectiveHooksDir, hookDiagnostics, installHooks, installGlobalHooks, uninstallGlobalHooks, globalHooksDir, installedHooks, remainingDispatchers, uninstallHooks, unrestoredChainedBackups } from '../src/githooks.mjs';
24
24
  import { ArgumentError, parseArguments } from '../src/args.mjs';
25
25
  import { engineForPolicy, scanGitTarget, scanMessage } from '../src/scan-target.mjs';
26
26
  import { resolvePolicy } from '../src/policy-resolver.mjs';
@@ -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
  }
@@ -1535,9 +1615,21 @@ function cmdUninstall(args) {
1535
1615
  console.error('aimhooman: not a git repository');
1536
1616
  return 30;
1537
1617
  }
1538
- return withLock(join(repo.commonDir, 'aimhooman-lifecycle.lock'), () => {
1618
+ const lifecycleLock = join(repo.commonDir, 'aimhooman-lifecycle.lock');
1619
+ const exitStatus = withLock(lifecycleLock, () => {
1539
1620
  const rep = uninstallHooks(repo);
1540
- removeExclude(repo.excludeFile);
1621
+ // The irreversible work is already done above, and the report is still
1622
+ // below. A throw from here unwound past all of it, so a damaged marker was
1623
+ // the only thing the user heard while four dispatchers kept guarding every
1624
+ // commit. Report the failure beside the removal report instead of in place
1625
+ // of it. Nothing is swallowed: this is also where the symlink and permission
1626
+ // guards surface, and their messages still reach the user and still exit 30.
1627
+ let excludeFailure = '';
1628
+ try {
1629
+ removeExclude(repo.excludeFile);
1630
+ } catch (error) {
1631
+ excludeFailure = `exclude block left in ${repo.excludeFile}: ${error.message}`;
1632
+ }
1541
1633
  // Trust the directory, not the report. Every refusal below leaves a working
1542
1634
  // dispatcher behind, and one printed under "uninstalled" reads as done.
1543
1635
  const remaining = remainingDispatchers(repo);
@@ -1552,6 +1644,7 @@ function cmdUninstall(args) {
1552
1644
  if (rep.restored.length) console.log(` hooks restored: ${rep.restored.join(', ')}`);
1553
1645
  for (const w of rep.warnings || []) console.log(` warning: ${w}`);
1554
1646
  for (const f of rep.failures || []) console.error(` failure: ${f}`);
1647
+ if (excludeFailure) console.error(` failure: ${excludeFailure}`);
1555
1648
  const unrestored = purge ? unrestoredChainedBackups(repo) : [];
1556
1649
  if (purge) {
1557
1650
  // Never wipe stateDir while a predecessor hook backup is still on disk:
@@ -1587,8 +1680,26 @@ function cmdUninstall(args) {
1587
1680
  console.log(' eligible non-bare repositories that inherit core.hooksPath are still guarded.');
1588
1681
  console.log(' run `aimhooman uninstall --global` to remove it.');
1589
1682
  }
1590
- return remaining.length || rep.failures?.length || unrestored.length ? 30 : 0;
1683
+ // The managed block is still in the exclude file and still ignoring paths, so
1684
+ // the uninstall is genuinely incomplete and 30 is the honest answer.
1685
+ return remaining.length || rep.failures?.length || unrestored.length || excludeFailure ? 30 : 0;
1591
1686
  }, LIFECYCLE_LOCK_OPTIONS);
1687
+ // Sweep the operational residue the guard authored in .git, so uninstall leaves
1688
+ // no aimhooman fingerprints behind — the same tooling residue this tool exists
1689
+ // to remove. rmdirSync deletes only an empty directory, so a live contender's
1690
+ // queue is never touched; the lifecycle queue becomes removable only now, after
1691
+ // its own lock above has released.
1692
+ for (const queue of [
1693
+ `${lifecycleLock}.queue`,
1694
+ `${repo.excludeFile}.aimhooman.lock.queue`,
1695
+ join(effectiveHooksDir(repo), '.aimhooman-hooks.lock.queue'),
1696
+ ]) {
1697
+ try { rmdirSync(queue); } catch { /* held by another aimhooman, or already gone */ }
1698
+ }
1699
+ // A one-commit-ago attribution backup that git's next COMMIT_EDITMSG makes stale.
1700
+ try { rmSync(join(repo.commonDir, 'COMMIT_EDITMSG.aimhooman-bak'), { force: true }); }
1701
+ catch { /* nothing to remove */ }
1702
+ return exitStatus;
1592
1703
  }
1593
1704
 
1594
1705
  function usage() {