Normally invoke install-sensitive-git-hooks and let the agent resolve paths,
run the installer, and verify the result. The direct command below is the
maintainer interface; Git invokes the installed hook entries during commits.
The sensitive commit protection is a pair of composable client-side Git hooks
installed by tools/sensitive_history/install_hooks.py.
For each target repository, Git's resolved hook directory contains:
hooks/
├── pre-commit
├── pre-commit.d/
│ ├── 50-existing optional preserved user hook
│ └── 90-sensitive managed staged-blob check
├── commit-msg
└── commit-msg.d/
├── 50-existing optional preserved user hook
└── 90-sensitive managed message check
The dispatcher marker is:
# llm-shared managed hook dispatcher v1
The managed entry marker is:
# llm-shared managed sensitive hook v1
Dispatchers invoke entries in filename order and stop at the first non-zero
status. Running the sensitive entry as 90-sensitive lets earlier hooks
modify the index or message before the final check.
<LLM_SHARED_PYTHON> <LLM_SHARED_DIR>/tools/sensitive_history/install_hooks.py [TARGET_REPO] [--shared-root LLM_SHARED_DIR] [--shared-rules RULES_FILE]
| Argument | Meaning |
|---|---|
TARGET_REPO |
Repository to protect; defaults to the current directory |
--shared-root |
llm-shared checkout containing the tracked adapters |
--shared-rules |
Common replacement-rules file stored in target-local Git config |
The generated 90-sensitive entries embed the absolute interpreter used to
run the installer. Invoke the installer with llm-shared's virtualenv Python so
the consuming repository needs no Python environment.
The installer compares the desired bytes with every managed file:
- correct files are not rewritten;
- stale dispatcher, adapter, interpreter, or shared-rules paths are repaired;
- an unmanaged main hook is moved once to
50-existing; - a preservation collision stops installation;
- a no-change run prints
HOOK: sensitive hooks already installed.
senv.bat runs the installer only for the managed repository the console
currently sits in, so startup pays for one check at most and repositories
outside the managed list show none. Each managed repository is therefore
checked the next time senv runs from inside it, not on every senv anywhere.
When --shared-rules is provided, the installer requires that file to exist
and writes its absolute path to
sensitive.sharedRulesFile in the target's local Git config.
The staged check compares the index with HEAD, or with Git's empty tree on
an unborn branch. It batch-reads only the new object IDs of added or modified
blobs.
It excludes:
- unstaged working-tree content;
- old commits, tags, paths, and historical blobs;
- deletions;
- pure renames whose blob ID is unchanged;
- submodule commit IDs.
A rename with content changes is scanned under its new path. Reused blob IDs are read once and associated with every pending path.
Git passes the final message-file path to commit-msg. The adapter scans that
file after templates, editor changes, -m arguments, amend operations, and
earlier message hooks have produced the pending message.
The hook reports only commit message line N; it does not echo matched text.
Both hooks merge the configured shared rules followed by the project-local rules described in Sensitive replacement-rules format.
A rejection prints redacted locations:
ERROR: sensitive content found in the pending commit:
- path/to/file.txt:12
Commit blocked; sensitive content was not printed.
| Status | Meaning |
|---|---|
0 |
The pending content is clean, or the existing rule files are empty and the hook printed a warning that protection is inactive |
1 |
Sensitive content was found; commit blocked |
2 |
Rules, repository state, Git, or message input could not be checked reliably |
An existing but empty shared-and-local rule set is a visible no-op so the
default bootstrap file does not prevent commits. Missing configured files,
unreadable or invalid rules, and other configuration failures still fail
closed. Git's --no-verify bypasses client-side hooks; enforce an equivalent
server-side check when bypass must be prohibited.
tools/sensitive_history/
├── install_hooks.py
├── sensitive_commit_check.py
├── sensitive_pre_commit.py
├── sensitive_commit_msg.py
└── history_scan.py
tests/unit/tools/sensitive_history/
├── test_install_hooks.py
└── test_sensitive_commit_check.py
Related: Installation guide, first-repository tutorial, and rationale.
