소스 정보
- 저장소
- lukemcqueen/hermes-cortex
- 최근 소스 활동
- 2026년 9월 1일 06:17
- 감지된 SKILL.md 언어
- 영어
- 스타
- 4
- 포크
- 1
설치 방법
기본적으로 소스를 먼저 확인하는 Prompt가 선택됩니다. 직접 명령으로 전환하거나 로컬 사본을 다운로드할 수도 있습니다.
소스 파일 검토
설치 여부를 결정하기 전에 SKILL.md와 SkillsMP에 표시된 보조 파일을 읽어 보세요.
메뉴
기본적으로 소스를 먼저 확인하는 Prompt가 선택됩니다. 직접 명령으로 전환하거나 로컬 사본을 다운로드할 수도 있습니다.
설치 여부를 결정하기 전에 SKILL.md와 SkillsMP에 표시된 보조 파일을 읽어 보세요.
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
직접 명령은 검토 Prompt를 거치지 않습니다. 실행하기 전에 소스를 확인하세요.
npx skills add https://github.com/lukemcqueen/hermes-cortex --skill enforcement-change-safety명령은 한 줄로 유지됩니다. 복사하기 전에 가로로 스크롤해 전체 내용을 확인하세요.
로컬 사본을 원하시나요? SkillsMP에서 현재 제공할 수 있는 파일을 다운로드하세요.
Cross-server agent health monitoring using binary status vectors — deploy health endpoints on each agent, poll from orchestrator, alert on state transitions.
Wire a self-hosted Langfuse instance to Hermes Agent — generate API keys, configure env vars, enable the bundled plugin, install SDK, and verify traces flow.
Mandatory pre-ship verification before calling end_change(). Covers Phase 0 survey, test, multi-OS, multi-role, docs, final verification, and reflexion. Every governance cycle must run this before closing.
SKILL.md 표시 중
| name | enforcement-change-safety |
| version | 1.0.0 |
| category | devops |
| description | Use before enforcement code changes or shared-repo commits. |
| author | Hermes Cortex |
| license | MIT |
| platforms | ["linux","macos"] |
| metadata | {"hermes":{"tags":["governance","enforcement","security","git","concurrency","hooks"],"related_skills":["enforcer-modification-considerations","change-checklist","two-hard-rules","loop-governance"]}} |
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).
When an issue says "remove the stale X block so the warning stops printing":
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.exit 1 with a
message pointing at the sanctioned fix (e.g. cortex-update.sh). A commit
without a governance record must not land.exit 0 would skip?exit 0 on missing dependency → the early exit IS
the bypass; convert to exit 1 (fail closed).bash -n the hook; live-test BOTH branches (dependency present → scoring
runs; dependency missing → exit 1).cortex-update.sh), verify deployed copy
byte-identical to repo source, verify fail-closed present in deployed.A hook that fails closed when a scorer binary is missing must actually FIND the binary on macOS, or every commit on Titus blocks:
~/.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.timeout command (coreutils provides gtimeout). Resolve
the timeout binary portably before the scorer invocation:
_TIMEOUT_BIN=""
if command -v timeout >/dev/null 2>&1; then _TIMEOUT_BIN="timeout 30"
elif command -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.for-in, command -v, [[ -z ]], $() are safe;
grep -P, mapfile, ${var,,} are NOT. Check the whole hook, not just your
edit.<<< herestrings and [[ =~ ]] — both are
bash-3.2-safe; do not "fix" them.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").
git status --short AND
git diff --cached --name-only. Review the FULL staged set.git restore --staged <file> — index-only,
worktree content untouched.git checkout -- ., git reset --hard) in a repo
others use — you destroy their uncommitted work.git restore --staged per foreign file.sha256sum verify) before any
recovery dance — proof nothing was lost.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.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.
mv .hermes-cortex/.governance-lock /tmp/x → tests pass
→ mv back. Phase 1 primary lock is separate, so your write gate survives.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.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).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.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:
git reflog -1 --format='%gs' — genuine git commit (normal, --no-verify,
--amend, --fixup) ALWAYS writes a message starting with commit
(commit: ..., commit (amend): ...). Internal replays write something
else: rebase (pick): ..., cherry-pick: ..., revert: ...,
merge <branch>: ..., pull ...:.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.
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"; do
if [[ -f "$candidate" ]]; then
ADVERSARIAL_SCRIPT="$candidate"
break
fi
done
if [[ -z "$ADVERSARIAL_SCRIPT" ]]; then
# fail CLOSED — a commit without the scan is a bypass
exit 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.Verify BEFORE shipping a hook change (all three):
ops/ tree → deployed copy found
(git init /tmp/proj && git config core.hooksPath ~/.hermes-cortex/hooks)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).
cd /tmp/repo && git commit --no-verify ... — hold a governance lock so the outer call passes),
and expect the gate's block message.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.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.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).
case "$FILE" in ...blocked_ips.add|...blocked_ips.submit) : ;; *) ...scan... ;; esac).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.sh refresh_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):
Pinned hooks fresh check: FAIL on stale → refresh → PASSPitfall: 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.
When a gate greps doctor output, the FAIL-detection pattern has bitten twice (2026-08-05, both caught by the dogfood loop itself):
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.❌ either. The footer 🔧 REQUIRED ACTIONS — resolve each ⚠️ or ❌ above contains ❌ mid-line → counted as a failure. The
gate blocked its own push showing "1 failure" while printing zero detail._DOCTOR_FAILS=$(echo "$DOCTOR_OUTPUT" | grep -E '^ *❌' \
| grep -vcE 'Overall: FAILING' || true)
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_TOP=$(git rev-parse --show-toplevel 2>/dev/null || echo "")
[[ "$_REPO_TOP" == "$CORTEX_REPO_TOP" ]] # CORTEX_REPO_TOP=${HOME}/hermes-cortex
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._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.
/usr/local/sbin (Linux) vs /usr/local/bin (macOS) — the doctor
check is platform-aware; the deploy script must match it.root (Linux) vs wheel (macOS — no root group).chattr/lsattr (Linux) vs chflags uchg/nouchg (macOS),
each guarded by command -v so a missing tool never fails the deploy.[ "$(uname -s)" = "Darwin" ].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:
Z (e.g. 2026-08-05T08:42:54Z).[: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._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 0was 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 GNUdate -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:
*.md, docs/) exempt — no deployed state changes.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.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.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):
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.register_seed lines as existence-only — PASS if present,
WARN if missing, NEVER content-compare a user-owned file.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:
_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:
-c/plain git passreferences/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.