Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
A direct command skips the review prompt. Inspect the source before running it.
Load BEFORE touching any enforcement/governance code: git hooks (pre-commit,
pre-push, post-commit), the enforcer plugin, the loop-governance MCP server, or
the scoring pipeline. Also load when doing ANY git operation in a repo that
other agent sessions share.
This skill exists because a single misread of a "fix the warning" request
deleted the entire scoring subsystem (the heart of loop governance), and a
careless git commit swept another session's staged work into the wrong commit.
Both were trust-destroying, user-corrected mistakes (2026-08-03).
Rule 1: NEVER Decrease Enforcement to Fix a Warning
When an issue says "remove the stale X block so the warning stops printing":
Remove the NOISE path, never the enforcement capability.
The stale part is usually a warn + exit 0 when a dependency is missing —
that exit 0 SHORT-CIRCUITS the hook early, skipping every downstream guard
(orchestrator-only paths, self-test, adversarial scan). THAT is the bug.
Fix: convert the missing-dependency path to FAIL CLOSED — exit 1 with a
message pointing at the sanctioned fix (e.g. cortex-update.sh). A commit
without a governance record must not land.
Luke's hard rule (2026-08-03): "NEVER DECREASE SECURITY TO ENFORCER". "Instead
of 'skipping', have the logic be FAIL IMMEDIATELY if the binary is gone."
The MCP server is the PRIMARY enforcement layer; the hook is secondary. But
"secondary" does NOT mean removable — the hook's scoring keeps the loop DB
populated and its guards catch direct-git bypasses. Deleting it eliminates the
entire function of the loops.
Checklist when asked to remove a "stale block"
Find the block's exact boundaries — what runs AFTER it that an early
exit 0 would skip?
If the block warns + exit 0 on missing dependency → the early exit IS
the bypass; convert to exit 1 (fail closed).
Keep every enforcement capability intact: scoring pipeline (task-id slug,
cycle auto-increment, code/prev file assembly, pass-rate, scorer
invocation, DB write, hard-block on failure), path guards, self-test,
adversarial scan.
Diff must show ONLY the block changed — zero lines of enforcement removed.
bash -n the hook; live-test BOTH branches (dependency present → scoring
runs; dependency missing → exit 1).
Deploy via the sanctioned path (cortex-update.sh), verify deployed copy
byte-identical to repo source, verify fail-closed present in deployed.
Rule 1b: macOS Portability of the Fail-Closed Hook
A hook that fails closed when a scorer binary is missing must actually FIND the
binary on macOS, or every commit on Titus blocks:
macOS has no ~/.local/bin in PATH by default — command -v score-cycle
fails even when installed. Add the canonically-deployed path as a search
candidate: $HOME/.hermes-cortex/tools/loop-governance/score_cycle.py
(cortex-update.sh registers it there on BOTH Linux and macOS). Test the search
with an emptied PATH to prove the deployed-path candidate resolves.
macOS has no timeout command (coreutils provides gtimeout). Resolve
the timeout binary portably before the scorer invocation:
_TIMEOUT_BIN=""ifcommand -v timeout >/dev/null 2>&1; then _TIMEOUT_BIN="timeout 30"elifcommand -v gtimeout >/dev/null 2>&1; then _TIMEOUT_BIN="gtimeout 30"fi
SCORE_OUTPUT=$($_TIMEOUT_BIN"$PYTHON_BIN""$SCORE_CYCLE" ...)
Empty _TIMEOUT_BIN = unbounded run; the || hard-block still guards.
macOS ships bash 3.2 — for-in, command -v, [[ -z ]], $() are safe;
grep -P, mapfile, ${var,,} are NOT. Check the whole hook, not just your
edit.
The pre-existing hook ALREADY uses <<< herestrings and [[ =~ ]] — both are
bash-3.2-safe; do not "fix" them.
Rule 2: Shared Repo = Check the Staged Set Before Every Commit
git add <my-file> does NOT mean only your file is staged. Sibling sessions
(and cron jobs) stage files concurrently into the SAME index. A plain
git commit sweeps EVERYTHING staged — foreign work lands in your commit and
its author panics ("my edits vanished").
Immediately before committing: git status --short AND
git diff --cached --name-only. Review the FULL staged set.
NEVER blanket-revert (git checkout -- ., git reset --hard) in a repo
others use — you destroy their uncommitted work.
After a soft reset, everything returns to the index; separate again with
git restore --staged per foreign file.
Backup worktree content to /tmp FIRST (sha256sum verify) before any
recovery dance — proof nothing was lost.
Verify against origin: git rev-parse HEAD origin/main,
git branch -r --contains <sha>. A sibling may have pushed a commit that
absorbed yours — confirm the final state on origin rather than fighting it.
Rule 3: Enforcer Test Contamination While Holding a Lock
tests/test_runtime/test_governance_bypass.py::TestHasGovernanceLock
(corrupted/deleted lock → False) FAILS when your session holds an active
governance lock: _has_governance_lock() Phase 3 reads the repo marker
.hermes-cortex/.governance-lock written by begin_change, so it returns True.
Prove contamination: mv .hermes-cortex/.governance-lock /tmp/x → tests pass
→ mv back. Phase 1 primary lock is separate, so your write gate survives.
Never call these a regression while mid-lock.
Rule 4: PENDING Cycles — Yours vs Others
begin_change creates a PENDING cycle. The doctor distinguishes (since
2026-08-05, commit 63981498): a cycle whose task_id has a LIVE lock file
(~/.hermes-cortex/state/.governance-*.json, status executing) is the
CURRENT task → reported INFO "score at end_change", NOT a FAIL. A cycle
whose task_id has NO lock is a LEAK (you moved on without scoring) → FAIL.
Green is achievable mid-lock; the current task's own cycle no longer fails
the doctor.
Score ALL your own cycles (feedback_accept) before end_change — and
score each task's cycle at THAT task's end_change. NEVER batch: opening
begin_change under a new task_id while earlier cycles from the same
session are still PENDING creates a leak (Luke caught 3 such cycles
2026-08-05; the 30-min backlog alert fires on exactly this).
Do NOT score a sibling session's or cron's cycles while they are paused or
mid-work — they own those. Enumerate with cycle_query(status="pending"),
score only the ones your session_id created.
cortex-update.sh no longer purges live locks (FIXED 2026-08-05). The
old "deploy purges locks" behavior was a TIMEZONE BUG, not a feature: the
stale-lock cleanup sliced the heartbeat to [:19], STRIPPING the ISO-8601
Z (UTC) marker, then date -d parsed UTC as LOCAL time — on UTC+9 hosts a
2-minute-old lock computed as 9h old → deleted on EVERY deploy. See Rule 12
for the full story. If a deploy still eats your lock, check the stale-lock
age math FIRST (a fresh lock showing hours of age = TZ bug), then re-acquire
with begin_change as a stopgap only.
Rule 5: Rebase/Cherry-Pick/Revert False "no-verify" Flag — FIXED via Reflog Discriminator (2026-08-04)
Symptom:git pull --rebase replays your commit WITHOUT running the
pre-commit hook, so the pre-commit sentinel (.git/.pre-commit-ran) is never
written. The post-commit hook then logs the NEW rebased hash in
~/.hermes-cortex/state/no-verify-log.json as a --no-verify commit, and
pre-push BLOCKS your push: "commit X was made with --no-verify". Cherry-pick
and revert hit the same false positive. It's a FALSE POSITIVE — the commit
went through the hook originally; the replay just bypassed it mechanically.
Root fix (committed 7bc86ca3):post-commit-audit now discriminates via
the HEAD reflog message instead of assuming sentinel-missing == bypass:
Logic: missing sentinel AND reflog starts with commit* → genuine bypass →
LOG. Missing sentinel but reflog is a replay prefix → silent, no log entry.
⚠️ Pitfall — use commit*, not commit:. The first implementation used
the prefix commit: which does NOT match commit (amend): ... — so
git commit --amend --no-verify (a GENUINE bypass) would have slipped through
silently. Prefix matching must be commit* so amend/no-verify still logs.
A real bypass must never be silenced to fix a false positive.
Workaround still needed ONLY on hosts whose deployed post-commit predates
the fix (before the next cortex-update.sh): git commit --amend --no-edit
re-runs the full pre-commit hook (sentinel written → consumed cleanly),
producing a new hash NOT in the log; then push passes. Leave the old dangling
entry — it can never match a future push range, and deleting audit entries
looks like tampering.
Do NOT:rm ~/.hermes-cortex/state/no-verify-log.json to unblock a push —
that is exactly the audit-trail tampering the pre-push hook exists to catch.
Verify hook behavior with the 8-path matrix before shipping any hook
change that touches the sentinel: scripts/test-post-commit-sentinel-matrix.sh
builds a scratch repo, installs the real hooks, and runs all eight paths
(normal, --no-verify, rebase, cherry-pick, revert, merge, amend, amend
--no-verify) asserting which must log and which must stay silent.
Rule 6: Hooks Run in EVERY Repo — Never Assume the Cortex Tree
core.hooksPath ~/.hermes-cortex/hooks is set globally — the pre-commit/
pre-push hooks fire in every git repo on the host (client-repo-a, client-repo-b,
client repos, any project without the cortex ops/ tree). A hook that builds
a path on $REPO_ROOT (the repo being committed IN) and assumes cortex
layout breaks EVERY commit in those repos — even one-line test fixes.
Real regression (2026-08-04, Esther, commit faa0e929): the adversarial
gate hard-resolved ADVERSARIAL_SCRIPT="$REPO_ROOT/ops/scripts/quality/adversarial-verify.py".
That path exists only in ~/hermes-cortex itself. Project repos have no ops/
tree → fail-closed block on every commit (Titus hit it on client-repo-a within
hours). Fix 72d6cdc3: candidate loop with deployed-path fallback.
The pattern — repo-local first, canonically-deployed second, fail CLOSED:
ADVERSARIAL_SCRIPT=""for candidate in"$REPO_ROOT/ops/scripts/quality/adversarial-verify.py" \
"$HOME/.hermes-cortex/scripts/adversarial-verify.py"; doif [[ -f "$candidate" ]]; then
ADVERSARIAL_SCRIPT="$candidate"breakfidoneif [[ -z "$ADVERSARIAL_SCRIPT" ]]; then# fail CLOSED — a commit without the scan is a bypassexit 1
fi
$HOME/.hermes-cortex/scripts/ is where cortex-update.sh registers every
deployed tool on BOTH Linux and macOS — always include it as the fallback.
$REPO_ROOT (from git rev-parse --show-toplevel) is the repo being
committed IN — only valid as the FIRST candidate, never the only path.
The existing score-cycle lookup (line ~576) already had this pattern — the
adversarial block just didn't follow it. When adding a tool lookup to a
hook, copy the established candidate-loop pattern, don't invent a new one.
Verify BEFORE shipping a hook change (all three):
Commit in the cortex repo → repo-local candidate wins (scan runs)
Commit in a scratch project repo with NO ops/ tree → deployed copy found
(git init /tmp/proj && git config core.hooksPath ~/.hermes-cortex/hooks)
Temporarily move the deployed tool aside → commit still blocked (exit 1),
then restore
Rule 7: Test Enforcement Gates with DIRECT Tool Calls — Never Subprocess-in-Script
The enforcer gates the TOP-LEVEL tool call only. Putting the command under test
inside a bash script (bash /tmp/test.sh whose body runs
git commit --no-verify ...) means the enforcer sees bash /tmp/test.sh —
the inner git command runs as a subprocess and NEVER crosses the gate. A test
that "proves" the gate is broken this way is testing nothing (2026-08-05: a
false "bypass-debt gate not working" conclusion; the gate was fine).
Run the exact command as a direct terminal call (cd /tmp/repo && git commit --no-verify ... — hold a governance lock so the outer call passes),
and expect the gate's block message.
For compound commands, the enforcer evaluates the WHOLE string — a
python3 -c '...' ; git commit --no-verify ... one-liner is gated as one
unit, so set up state (debt file, marker) in a SEPARATE call first.
Unit-test the gate's classifier functions directly by importing the deployed
module (spec_from_file_location on
~/.hermes/plugins/governance-enforcer/__init__.py) and calling
_is_readonly_terminal_command, _bypass_debt_count, re.search(...) —
fast, deterministic, no repo needed.
Rule 8: Content Scanners Need Narrow Path Exemptions for By-Design Data Files
PII/content scanners (enforcer PII gate, secret-leak-detector) must exempt
files whose PURPOSE is to hold the flagged content — with a NARROW path
allowlist, never a blanket pattern disable. The shared blocklist
(ops/install/deploy/nginx/blocked_ips.add / .submit) exists to hold PUBLIC
IPs; that is the data, not PII. Without the exemption every commit staging the
file warns once per IP — Gisu got flooded with dozens of ⚠ PII — public IP address warnings per commit (Telegram spam-filter ban risk), and the
pipeline's own sanctioned commits generated the noise too (2026-08-05).
Exempt by exact repo-relative path (case "$FILE" in ...blocked_ips.add|...blocked_ips.submit) : ;; *) ...scan... ;; esac).
Everything else must stay scanned — the exemption is a 2-line case, not an IP pass.
Prove RED-GREEN: same IPs in the exempted file → 0 warnings; same IPs in a
normal file → warnings still fire.
Rule 9: Pinned Repos + the hooksPath Guard — Refresh Files AND Carve Out the Guard
pin_repos_with_own_hooks() sets a repo's local core.hooksPath to its OWN
.git/hooks (to preserve deploy-bare-repo hooks) but historically never
refreshed the hook FILES — stale copies predated the mandatory adversarial
gate (Titus audit 2026-08-05: 9 repos, grep -c adversarial = 0). Two things
are needed together, or the fix breaks commits:
Refresh the files: cortex-update.shrefresh_pinned_hook_files()
copies the 4 cortex hooks (pre-commit-score, pre-push-pull,
post-commit-audit, post-push-audit) from deployed source into the repo's
own hooks dir. ONLY files carrying the cortex banner (Git <type> hook,
ASCII match — locale-safe on macOS) are overwritten; foreign hooks (vllm
pre-commit framework shim) are preserved. Missing hook files get the gate
installed.
Carve out the hooksPath guard: pre-commit-score and pre-push-pull both
fail CLOSED when core.hooksPath != ~/.hermes-cortex/hooks (5ab54547).
That guard would block EVERY commit in a pinned repo (their hooksPath IS
their own dir). The carve-out passes when the hooks dir carries any
cortex-managed hook (governance IS running there); the tripwire still
fires when hooksPath points at a dir with no cortex hooks.
Verify before shipping a pinned-hooks change (all three):
Scratch repo with stale cortex hook → after refresh, copy byte-matches
deployed and commit passes the hooksPath guard (fails later at the
governance lock — that's fail-closed working)
Repo with a FOREIGN hook → refresh skips it, hash unchanged
Doctor Pinned hooks fresh check: FAIL on stale → refresh → PASS
Pitfall: deployed hook files are chattr +i immutable — you cannot
overwrite them by hand to test; use cortex-update.sh or test with
repo-source as the simulated deployed source.
Rule 10: Leaked PENDING Cycles Must Block the Push — and Doctor-Output Greps Are Traps
When a gate greps doctor output, the FAIL-detection pattern has bitten twice
(2026-08-05, both caught by the dogfood loop itself):
Never grep for the literal word FAIL. The doctor's summary line is
❌ Overall: FAILING — which contains "FAIL" → false positive on every
green run. And ❌ PENDING cycles contains no "FAIL" → false negative.
Never bare-grep ❌ either. The footer 🔧 REQUIRED ACTIONS — resolve each ⚠️ or ❌ abovecontains ❌ mid-line → counted as a failure. The
gate blocked its own push showing "1 failure" while printing zero detail.
Correct pattern (both pre-push gate and cortex-dogfood.sh):
Match ^ *❌ (lines STARTING with ❌ = actual check lines), exclude ONLY
the summary line. Unit-test against REAL doctor output including the
footer — a synthetic fixture without the footer line hides the bug.
Leaked-cycle enforcement (63981498): the doctor now FAILs only on cycles
from FINISHED tasks (no active lock for their task_id); the current task's
cycle (lock held) is INFO. The pre-push gate therefore must NOT exclude
"PENDING cycles" lines — a push with leaked prior-task cycles is BLOCKED.
That is the mechanism that makes "clear cycles during cleanup, not later"
(Luke directive 2026-08-05) physical: you cannot ship while old cycles sit
unscored. The same applies to cortex-dogfood.sh — its verify step counts
the same way.
Two Luke corrections (2026-08-05) about gate blast radius. The pre-commit and
pre-push hooks fire in EVERY repo on the host (Rule 6) — so any NEW gate added
to them must be doubly scoped or it will block innocent work in project repos:
Repo scope: only fire in the hermes-cortex repo itself.
Non-cortex repos (client-repo-a, client-repo-b, client-web-app, pinned repos,
...) must NEVER run the hermes-cortex doctor — a failing cortex state must
not block a client repo's push.
Orchestrator scope: only orchestrator hosts run orchestrator-level
gates. Reuse _detect_orch() (hostname moses|esther AND home
/home/<hostname>, env-independent — the SAME function pre-commit uses
for its self-test; copy it, don't reinvent). Non-orch hosts never run the
doctor gate. cortex-dogfood.sh exits 1 with a clear message on
non-orch hosts.
Orchestrator-only path arrays must be repo-scoped too (Titus over-block):
the hardcoded ORCHESTRATOR_ONLY_PATHS array had UNANCHORED patterns
(test_.*\.py$, .*_test\.py$, .*_spec\.py$) that fired in every repo —
Titus was blocked committing apps/api/tests/test_ipi_similarity.py in a
project repo. The config-driven guard (docs/orchestrator-only-paths.txt read
from HEAD) is the correct repo-aware design: "no config file = no
restrictions". The hardcoded array must be wrapped in the same
if [[ "$REPO_ROOT" == "${HOME}/hermes-cortex" ]] gate, and any
cortex-specific path it protects that the config misses (e.g.
^core/governance/tests/) added explicitly rather than via unanchored
patterns.
macOS deploy-script portability (deploy-fix-blocked-ips.sh, 2026-08-05):
a deploy script that hardcodes Linux assumptions breaks on macOS silently —
the sanctioned fix lands in the wrong dir and the doctor FAIL persists.
DEST: /usr/local/sbin (Linux) vs /usr/local/bin (macOS) — the doctor
check is platform-aware; the deploy script must match it.
Group: root (Linux) vs wheel (macOS — no root group).
Immutability: chattr/lsattr (Linux) vs chflags uchg/nouchg (macOS),
each guarded by command -v so a missing tool never fails the deploy.
Sudoers template: list BOTH platform paths — a path that doesn't exist on
a host simply never matches (harmless dead entry on the other platform).
Detect platform ONCE at top: [ "$(uname -s)" = "Darwin" ].
Rule 12: TZ Bug in Lock-Age Math Kills Live Locks — and Mandatory Dogfood Runs the Deploy
The TZ bug (2026-08-05, Luke: "this lock issue is MISERABLE"): the "cortex-
update purges locks every deploy" behavior was NOT a designed purge — it was a
timezone parse bug in cortex-update.sh's stale-lock cleanup:
The heartbeat is ISO-8601 UTC ending in Z (e.g. 2026-08-05T08:42:54Z).
The cleanup sliced it to [:19] — stripping the Z — then ran
date -d "$heartbeat" +%s, which parses a marker-less timestamp as LOCAL
time. On a UTC+9 host (KST) the UTC time parsed as +9h-offset LOCAL → a
FRESH 2-minute lock computed as 9h old → > 3600 threshold → deleted
on every deploy.
The enforcer's _has_governance_lock (Python datetime.fromisoformat)
handled Z correctly the whole time — the lock was being deleted under it.
Fix pattern for ANY bash date -d on an ISO heartbeat: KEEP the Z —
date -d "2026-08-05T08:42:54Z" +%s parses correctly. Or use Python
(fromisoformat handles Z). When diagnosing a "purged" lock, verify the
age math FIRST: a lock minutes old showing hours of age = TZ bug, not a real
stale lock. Python purge paths (MCP server, purge-stale-governance-locks.py)
were already correct — but the bash date -d had a SECOND, worse bug
(2026-08-10, Titus): date -d is GNU-only — macOS BSD date has no -d,
so the 2>/dev/null || echo 0 fallback fired, epoch=0, age = now-0 ≈ 1.78e9s
3600 → every lock, including fresh v2 session locks, deleted on every
macOS deploy (cortex-update.sh:2719). The TZ fix (keep the Z) only addressed
GNU parse — the || echo 0 was the fail-OPEN trap. Fix that shipped:
portable python3 epoch (datetime.fromisoformat(hb.replace('Z','+00:00')))
with an EMPTY fallback (|| echo "") + [[ -n "$epoch" ]] guard — parse
failure now SKIPS the lock (P1-A rule: never delete what you can't
age-verify). Rule: any heartbeat→epoch conversion in bash must (a) parse
ISO-8601 with Z via python3, not GNU date -d, and (b) fail CLOSED on parse
failure — empty string + skip, never || echo 0 + delete.
Mandatory dogfood in the pre-push gate (Luke: "make this MANDATORY — I
thought you did already"): a push that touches ANY non-doc file in
hermes-cortex runs the FULL dogfood cycle (pull → cortex-update → doctor →
verify) BEFORE landing. The gate invokes cortex-dogfood.sh --quiet itself
so the step cannot be forgotten:
Scope (both Luke corrections 2026-08-05): hermes-cortex repo ONLY
(dogfood = redeployment of HC — irrelevant to client repos) AND orchestrator
host only. Docs (*.md, docs/) exempt — no deployed state changes.
Trigger is INVERTED, not an allowlist: fire on every non-doc file
INCLUDING path changes/renames/config refs. A narrow pattern list missed
renamed scripts.
Own-task exemption: the dogfood deploy purges the governance lock (the
TZ bug's orphan behavior, even after the fix the deploy legitimately
reloads the enforcer plugin), so the doctor sees THIS session's own cycle
as "leaked" (no lock). cortex-dogfood.sh captures the active task_id
(DOGFOOD_OWN_TASK) BEFORE deploy and exempts exactly that task's cycle
from the FAIL count — other leaked cycles still fail.
Skill reloads are NOT lock-coupled: the per-session skills marker
(state/skills-loaded/<session_id>) survives deploy; the reload trigger is
deployed-skill drift → doctor → re-skill_view, not lock deletion. Do not
delete locks to force skill reloads — invalidate the marker instead.
Rule 13: User-Owned Deploy Destinations — Seed-Guard, Never Hash-Overwrite
A register() dest that is USER-OWNED after install (MEMORY.md, USER.md, any
seed template the user personalizes) must never ride the generic
needs_update() hash-overwrite path. A personalized MEMORY.md can never match
its template, so EVERY full-mode deploy (the default; the post-merge hook
auto-runs cortex-update.sh) clobbers it — 7 clobbers in one day (2026-08-05),
saved only by deploy-backups/*.bak + manual restore.
The doctor made it worse — an INVERTED check: check_deploy_checksums
Category 1 parses every register line and content-compares deployed vs repo.
Personalized memory → FAIL + "Run: cortex-update.sh to resync", which is
EXACTLY the destructive action. The broken state (blank seed) PASSED; the
healthy state (personalized) FAILED. Do NOT "fix" the doctor FAIL by resyncing —
fix the classification: user-owned files are not repo-managed files.
Fix pattern (both layers or the bug persists):
Deploy: register_seed() — copy ONLY when dest is missing, in BOTH full
and delta modes. Keep seed dests out of MAP/ORCH_MAP, and cover them in
clean_stale_deploys / check_stale_deploys so they aren't flagged stale.
Doctor: parse register_seed lines as existence-only — PASS if present,
WARN if missing, NEVER content-compare a user-owned file.
A "guarded — only if dest missing" comment on a plain register line is a
LIE until the code enforces it — the false comment is how this bug hid.
Topology for diagnosis: live memory = ~/.hermes/memories/MEMORY.md
(Hermes resolves get_hermes_home()/memories, default HERMES_HOME=~/.hermes).
The deploy target ~/.hermes-cortex/memories/MEMORY.md is a dead seed copy on
most hosts — live only when HERMES_HOME=$HOME/.hermes-cortex (commented
option in hermes-cortex.env.example) or ~/.hermes/memories is symlinked to
it. Check which path the host loads BEFORE diagnosing "memory wiped": a
checksum "fix" on the dead copy is a no-op; on the live copy it is data loss.
Three governance gaps found and closed by the 2026-08-08 edge-case audit
(Esther, Luke: "test governance thoroughly, see the weaknesses"):
14a. Skills tracking was PROCESS-global — sessions leaked each other's loads._skills_loaded_in_session is one module-level set shared by every session in
the gateway process. Any session's skill_view() counted for ALL sessions:
The 7-skill marker auto-created for a session that loaded only 2 skills
(other sessions contributed the rest) → write tools unblocked without the
session actually loading the skills.
The domain gate (_check_domain_skill_gate) and adversarial gate passed
because ANOTHER session had loaded the skill → on long turns agents never
loaded mid-turn domain skills; the gate passed anyway.
Fix: per-session registry _session_skills_loaded: dict[str, set], populated
in the pre_tool_call hook on skill_view, consulted by the marker auto-create
condition, domain gate, and adversarial gate. Each session must load its own
7 always-skills and its own domain skill. Tests:
TestPerSessionSkillIsolation (marker + adversarial) and
test_md_write_blocks_when_skill_loaded_by_other_session (domain gate).
14b. git -c core.hooksPath=... / GIT_CONFIG_GLOBAL|SYSTEM=... bypassed the
entire hook chain. The bypass-debt regex only matched literal --no-verify.
A per-invocation -c core.hooksPath=/dev/null commit skipped EVERY hook
including post-commit-audit, so the debt counter never incremented and the
escape hatch was unbounded. Fix: the enforcer now blocks hook-override forms
outright (they are not the sanctioned escape hatch); --no-verify remains
bounded by the debt counter (3 tolerated, 4th+ mandated). Benign -c configs
(user.name, color.ui) and plain git commit/status are NOT matched.
Tests: TestGitHookBypassGate (10 override forms detected, 7 benign forms
clean).
14c. Sessions could stack unbounded PENDING cycles.end_change() only
WARNED when the task's cycle was unscored, then released the lock; begin_change()
only checked for an existing lock. Sequence begin(A) → end(A) unscored →
begin(B) succeeded, leaving A PENDING until the doctor blocked the push.
Fix (Luke: "agents close out/score before moving to a new task"):
end_change() BLOCKS releasing the lock while the task's latest cycle is
unscored (user_overrode IS NULL, decision PENDING/LOOP).
begin_change() REFUSES a new task while THIS session still has unscored
PENDING cycles (hook cycles with session_id NULL are exempt).
Tests: tests/test_runtime/test_mcp_closeout.py
(TestEndChangeRequiresScoredCycle, TestBeginChangeCloseOutGate).
14d. _skills_dir() resolved to a NONEXISTENT dir when HERMES_HOME is set —
the fingerprint gate was a constant in production. The gateway runs with
HERMES_HOME=/home/<user>/.hermes; _skills_dir() returned
HERMES_HOME/.hermes/skills = ~/.hermes/.hermes/skills (no such dir), so
_skills_fingerprint() hashed eight EMPTY mtimes — a constant that never
changed. Markers never went stale after deploys, so agents were never forced
to reload the always-skills mid-turn (the 2026-08-05 skills-before-task gate
was silently dead). Also, task-start lives under workflow/, which the
fingerprint candidate paths missed. Fix: _skills_dir() resolves
HERMES_HOME/skills when it exists (HERMES_HOME IS the .hermes dir), and the
candidate list includes workflow/. Regression tests: TestSkillsDirResolution
(3 env variants + fingerprint-mtime-change).
Checklist when touching these paths:
Per-session skill tests: session A's loads never satisfy session B
Hook-override regex: override forms block, benign -c/plain git pass
Close-out: unscored end_change keeps the lock; begin_change refuses
with prior PENDING; scored flow releases cleanly
Hook cycles (session_id NULL) never trip the begin_change gate
References
references/memory-seed-clobber-2026-08-05.md — the memory-clobber root
cause chain (register comment lie → needs_update hash path → doctor Category 1
sweep → inverted doctor), the live-vs-seed topology, and the register_seed +
existence-only doctor fix design.
references/tz-bug-lock-purge-and-mandatory-dogfood-2026-08-05.md — the TZ
root cause with exact before/after age math, the mandatory-dogfood gate
scope corrections, and the own-task exemption design.
references/leaked-cycles-and-doctor-grep-2026-08-05.md — the leaked-cycle
enforcement design (active-lock split), the doctor-output grep trap with
exact failing/working patterns, and the repo+orch scoping recipe.
references/pre-commit-score-fail-closed-2026-08-03.md — the incident:
misread → deletion → revert → correct fail-closed fix, with exact commands.
references/macos-fail-closed-hook-2026-08-03.md — macOS portability of the
fail-closed hook: deployed-path scorer candidate, timeout→gtimeout
fallback, bash-3.2-safe construct list, deploy+verify cycle for both OSes.
scripts/test-post-commit-sentinel-matrix.sh — re-runnable 8-path matrix
(normal / --no-verify / rebase / cherry-pick / revert / merge / amend /
amend --no-verify) proving genuine bypasses still log and internal replays
stay silent. Run before shipping any sentinel-touching hook change.