|
| 1 | +# `.githooks/` — composable, vendor-neutral ritual triggers |
| 2 | + |
| 3 | +These are **committed, vendor-neutral git hook dispatchers and fragments** that reinforce the |
| 4 | +after-session ritual for *any* agent (Claude, Copilot, Kiro, …) — because everyone commits, |
| 5 | +regardless of which AI did the work. They are **advisory by default** — the pre-commit secret guard |
| 6 | +alone **enforces** (findings block the |
| 7 | +commit; a deliberate, scoped exception, because secrets carry irreversible after-the-fact cost) — |
| 8 | +and the **tool runs nothing itself**: git invokes them in your env at your |
| 9 | +opt-in (`no-build-step-agent-run`). See `docs/optional-ritual-hook.md` and `DECAY.md` for the rationale. |
| 10 | + |
| 11 | +## First-run init (one command) |
| 12 | + |
| 13 | +A fresh clone has the gitignored skill **adapters absent** and the dispatchers **unactivated** (git can't |
| 14 | +auto-run committed hooks on clone — security). Set both up with one idempotent command: |
| 15 | + |
| 16 | +```sh |
| 17 | +bash .githooks/init.sh |
| 18 | +``` |
| 19 | + |
| 20 | +It regenerates the vendor skill adapters **and** runs `git config core.hooksPath .githooks`. **The agent |
| 21 | +runs this itself on a first session** (see `memory/PROTOCOL.md`), so an untrained user does nothing. To |
| 22 | +activate the dispatchers alone: `git config core.hooksPath .githooks` (undo: |
| 23 | +`git config --unset core.hooksPath`). |
| 24 | +**CI is the zero-config floor** — `.github/workflows/agent-memory.yml` on GitHub; `.gitlab-ci.yml` + |
| 25 | +`.gitlab/agent-memory-ci.yml` on GitLab (v4.31.0); `.azuredevops/agent-memory-ci.yml` on Azure |
| 26 | +DevOps (v4.32.0) — it runs server-side on every push and, on GitHub/GitLab, every pull/merge |
| 27 | +request (Azure DevOps PR-time runs need the optional Build Validation policy), so the ritual is |
| 28 | +enforced even on a clone where the local hook was never activated. (Honest limits: a self-managed GitLab instance needs an admin-registered runner; |
| 29 | +an Azure DevOps pipeline is inert until its one-time `az pipelines create` binding.) |
| 30 | + |
| 31 | +## Dispatcher contract |
| 32 | + |
| 33 | +The `pre-commit` and `post-commit` entrypoints are deliberately small dispatchers. Each runs the |
| 34 | +executable regular files in its matching directory — `.githooks/pre-commit.d/` or |
| 35 | +`.githooks/post-commit.d/` — in deterministic C-locale filename order. Hidden and non-executable |
| 36 | +files are ignored. Hook arguments are forwarded unchanged. |
| 37 | + |
| 38 | +Every fragment runs even when an earlier fragment fails, so independent hook layers all get a |
| 39 | +chance to report. The dispatcher returns the **first non-zero status** after the run. That blocks a |
| 40 | +pre-commit when any enforcing fragment fails; Git ignores post-commit status, while direct invocation |
| 41 | +and tests can still observe failures. |
| 42 | + |
| 43 | +Agent-memory's managed fragments use the `50-` slot: |
| 44 | + |
| 45 | +- `pre-commit.d/50-agent-memory-secret-guard` |
| 46 | +- `post-commit.d/50-agent-memory-ritual-capture` |
| 47 | + |
| 48 | +Add another executable fragment instead of replacing either hook entrypoint: use `00-`–`49-` to run |
| 49 | +before agent-memory or `51-`–`99-` to run after it. Upgrades refresh unchanged copies of the two |
| 50 | +dispatchers and managed `50-` fragments, preserve every differently named fragment, and human-gate |
| 51 | +any locally modified managed file instead of silently overwriting it. This gives other hook layers |
| 52 | +a stable composition seam and makes each behavior independently runnable in CI. |
| 53 | + |
| 54 | +The dispatcher contract itself is covered by `tests/test_githook_dispatchers.sh` in the tool repo. |
| 55 | + |
| 56 | +## Managed fragments |
| 57 | + |
| 58 | +- **`pre-commit.d/50-agent-memory-secret-guard`** (v4.34.0) — the **`[secret-material]` guard**: |
| 59 | + before the commit exists, scans the |
| 60 | + **staged content** (the index, not the worktree — exactly what this commit would publish; a |
| 61 | + pre-existing finding elsewhere never gates an unrelated commit) of **two surfaces**: |
| 62 | + `memory/**.md` (the full profile — credentials + PII) and **config files** — `.json` / `.yml` / |
| 63 | + `.yaml` / `.properties` / `.toml` / `.ini` / `.env*` anywhere in the repo, credential-class checks |
| 64 | + only (token shapes, key assignments, Authorization headers, private keys; config files |
| 65 | + legitimately carry contact emails and paths). The config surface exists because of a real |
| 66 | + incident: live credentials entered a repo inside a Postman JSON and an OpenShift YAML, then |
| 67 | + contaminated a session log downstream. Findings print with the linter's non-echoing report plus |
| 68 | + redaction/waiver/rotation guidance. **Enforcing by default** — findings **block the commit** |
| 69 | + (secrets are the one category with irreversible after-the-fact cost); opt down to warn-only with |
| 70 | + `AGENT_MEMORY_SECRET_GUARD=advisory` (env, or `git config agent-memory.secretguard advisory`); |
| 71 | + one-off bypass: `git commit --no-verify`. |
| 72 | + Waivers: tag the line `lint:allow-secret-material` where the format has comments (markdown, |
| 73 | + YAML, TOML, INI); JSON has no comments and a `.properties` same-line comment corrupts the value — |
| 74 | + list those files in the committed, human-audited **`.agent/secret-scan-ignore`** (shell-glob per |
| 75 | + line; exempts config files only, never `memory/`). Runs on python3 or node, whichever exists — |
| 76 | + with neither, it skips with a note. Why it exists: the ritual rule covers agents at write time |
| 77 | + and the CI floor covers pushes, but by push time the remote already has the secret and redaction |
| 78 | + is not un-leaking (rotation is) — this is the **one placement that prevents instead of detects** |
| 79 | + (see the `memory/PROTOCOL.md` redaction rule). |
| 80 | + |
| 81 | +- **`post-commit.d/50-agent-memory-ritual-capture`** — after a commit: re-syncs skill adapters if a |
| 82 | + skill changed; and if the commit did |
| 83 | + real work but carried no session log, ensures the session is captured — **once per working session, not |
| 84 | + per commit.** If there is **no** session log within the active-session window (default **30 min**; override |
| 85 | + `AGENT_MEMORY_SESSION_WINDOW_MINUTES`) it **auto-stubs** `memory/sessions/<ts>.md`; if a recent log already |
| 86 | + covers this session — committed *or* a waiting stub, detected by the newest session **filename** (immutable |
| 87 | + and clone-safe, unlike mtime) — it instead **nudges you to enrich that existing log**. The stub guarantees |
| 88 | + the ledger never has a silent gap; the *thoughtful* summary stays the agent's job (capture vs. judgment — |
| 89 | + same split as `memory-lint`). |
| 90 | + |
| 91 | + > **Splitting code and memory into two commits?** The advisory may fire on the code-only commit (it |
| 92 | + > carries no session log) and point you at the session's existing log — **expected and benign**, not a |
| 93 | + > failure, and it will **not** pile up a second stub (one log per session). To skip the nudge entirely, |
| 94 | + > prefer a **single atomic commit** that includes the work *and* its session log. The hook is advisory |
| 95 | + > and never blocks. |
| 96 | +
|
| 97 | +To deactivate: `git config --unset core.hooksPath`. |
0 commit comments