Skip to content

feat(devops): config-integrity-watchdog — git-backed config fingerprinting - #67

Merged
github-actions[bot] merged 4 commits into
mainfrom
feat/config-integrity-watchdog
Jul 2, 2026
Merged

feat(devops): config-integrity-watchdog — git-backed config fingerprinting#67
github-actions[bot] merged 4 commits into
mainfrom
feat/config-integrity-watchdog

Conversation

@dizhaky

@dizhaky dizhaky commented Jul 2, 2026

Copy link
Copy Markdown
Owner

Summary

Replaces the mutable .sha256 sidecar file with a git-backed, append-only integrity log stored in the dotfiles repository. The Config Integrity Watchdog has triggered 25+ times in 19 days because any process that can write config.yaml can also overwrite the sidecar — silently masking the tampering. A git commit cannot be silently forged by a process without git credentials.

What ships

skills/devops/config-integrity-watchdog/ — new skill (auto-synced by skills_sync):

  • scripts/seal.py — hashes config, appends to config_integrity.jsonl, commits to dotfiles git
  • scripts/verify.py — compares current hash to latest seal; exit 1 if tampered, exit 2 if log itself modified
  • scripts/restore.pygit checkout HEAD to revert config, backs up tampered file, re-seals
  • config_integrity.py — shared core module imported by both scripts and CLI

hermes_cli/config_integrity_cli.py + wiring in config.py/main.py — new CLI subcommands:

hermes config seal      # establish or refresh baseline
hermes config verify    # check integrity (exit 0=ok, 1=tampered, 2=log tampered, 3=no baseline)
hermes config restore   # revert to baseline

.plans/config-integrity-watchdog.md — project tracking doc (moved from PLAN.md)

Configuration

Env var Default Purpose
HERMES_CONFIG ~/.hermes/config.yaml Config file to protect
HERMES_DOTFILES_DIR ~/Dev/dotfiles Dotfiles git repo root

Tests

  • 29 skill-level tests (tests/skills/test_config_integrity_watchdog.py)
  • 21 CLI integration tests (tests/hermes_cli/test_config_integrity_cli.py)
  • All 50 pass

Test plan

  1. hermes config seal — should print ✅ and exit 0
  2. hermes config verify — should print OK and exit 0
  3. Manually edit config.yaml; hermes config verify — should print 🚨 and exit 1
  4. hermes config restore — should revert config, print ✅, exit 0
  5. hermes config verify — should pass again

Slack thread

https://mfc-nyc.slack.com/archives/C0BD8QBUSJF/p1782742870774319


Generated by Claude Code

claude added 4 commits July 2, 2026 04:17
…gerprinting

Replaces mutable .sha256 sidecar with an append-only integrity log
committed to the dotfiles git repo. A process without git commit
credentials cannot silently forge a fingerprint entry.

- seal.py: hash config + append to log + git commit
- verify.py: compare current hash against latest seal; detect log tampering
- restore.py: revert from git + re-seal; backs up tampered config
- PLAN.md: project plan (Linear not available)
- Tests: seal->verify, seal->tamper->verify, seal->tamper->restore->verify (29 passing)

Closes the 19-day recurring config hijack pattern (Event #25+).
Slack: https://mfc-nyc.slack.com/archives/C0BD8QBUSJF/p1782742870774319

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PQKCc5mDedYAiCNyXnTezh
…x match

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PQKCc5mDedYAiCNyXnTezh
Integrates config-integrity-watchdog into the Hermes CLI so users
can seal, verify, and restore config integrity without remembering
script paths. Calls core logic shared with the standalone scripts.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PQKCc5mDedYAiCNyXnTezh
…caveat

- _import_core() now tries ~/.hermes/skills first, then repo-relative
  skills/ as fallback for pre-sync usage; prints clear error + exits 1
  if skill not found in either location
- Move PLAN.md to .plans/config-integrity-watchdog.md
- Append git-HEAD caveat to restore.py module docstring

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PQKCc5mDedYAiCNyXnTezh
@github-actions
github-actions Bot merged commit 38d8fdb into main Jul 2, 2026
21 of 27 checks passed
@github-actions
github-actions Bot deleted the feat/config-integrity-watchdog branch July 2, 2026 04:19
@github-actions

github-actions Bot commented Jul 2, 2026

Copy link
Copy Markdown

🔎 Lint report: feat/config-integrity-watchdog vs origin/main

ruff

Total: 0 on HEAD, 0 on base (➖ 0)

🆕 New issues: none

✅ Fixed issues: none

Unchanged: 0 pre-existing issues carried over.

ty (type checker)

Total: 8613 on HEAD, 8607 on base (🆕 +6)

🆕 New issues (4):

Rule Count
unresolved-import 3
unresolved-attribute 1
First entries
hermes_cli/config_integrity_cli.py:47: [unresolved-attribute] unresolved-attribute: Object of type `Action` has no attribute `add_parser`
hermes_cli/config_integrity_cli.py:78: [unresolved-import] unresolved-import: Cannot resolve imported module `config_integrity`
tests/skills/test_config_integrity_watchdog.py:19: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`
tests/hermes_cli/test_config_integrity_cli.py:24: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`

✅ Fixed issues: none

Unchanged: 4576 pre-existing issues carried over.

Diagnostics are surfaced as warnings — this check never fails the build.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9c3bdb0dd8

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".



def _default_config_path() -> Path:
return Path(os.environ.get("HERMES_CONFIG", "~/.hermes/config.yaml")).expanduser()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Use the active Hermes home for default config paths

In profile runs such as hermes -p work config seal, _apply_profile_override() sets HERMES_HOME, not HERMES_CONFIG, so this default still hashes/restores ~/.hermes/config.yaml from the default profile. That can seal or overwrite the wrong profile's config; default to get_hermes_home()/config.yaml (and mirror this in the scripts) unless HERMES_CONFIG is explicitly set.

Useful? React with 👍 / 👎.

log_path = _log_path(dotfiles_dir)
canonical_config = _canonical_config_path(dotfiles_dir)

baseline = load_baseline_entry(log_path)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Refuse restore when the integrity log is dirty

When verify returns 2 for uncommitted log changes, restore can still be run and it loads the last uncommitted seal entry here. If a tampering process appends a seal for the tampered config, restore sees current_hash == baseline_hash, does a no-op, and exits 0; add the same log_has_uncommitted_changes guard before trusting the baseline.

Useful? React with 👍 / 👎.

Comment on lines +46 to +48
subprocess.run(
["git", "add", str(log_path.relative_to(dotfiles_dir))],
cwd=dotfiles_dir, check=True, capture_output=True,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Stage the config snapshot when sealing

The documented flow says to run seal after intentional config changes, but this helper only stages config_integrity.jsonl. If hermes/config.yaml changed in the dotfiles repo (or the protected config is copied there) but was not committed separately, the committed baseline hash advances while HEAD:hermes/config.yaml remains old, so restore() checks out stale content and the next verify still fails.

Useful? React with 👍 / 👎.

Comment on lines +299 to +305
new_hash = hash_file(config_path)
if new_hash != baseline_hash:
print(
f"WARNING: Restored hash {new_hash[:16] if new_hash else 'None'} "
f"doesn't match baseline {baseline_hash[:16]}",
file=sys.stderr,
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Return failure when restore misses the sealed hash

If git checkout succeeds but produces content whose hash does not match the latest seal (for example because HEAD has an older config than the log), this branch only warns and then appends a restore entry and returns 0. Cron/CLI callers will treat the restore as successful even though verify remains failing, so this mismatch should stop with a non-zero status.

Useful? React with 👍 / 👎.

related_skills: [ugw-health-check]
---

## When to Use

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Add the required skill title and intro

The root AGENTS.md skill standards require the body order to start with # <Skill> Skill and a 2–3 sentence intro before ## When to Use; this new SKILL.md begins directly with ## When to Use, so it violates the documented hardline skill-review checklist despite valid frontmatter.

Useful? React with 👍 / 👎.

Comment on lines +56 to +58
subprocess.run(
["git", "commit", "-m", message],
cwd=dotfiles_dir, check=True, capture_output=True,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Limit integrity commits to owned paths

When a user already has unrelated files staged in the dotfiles repo, this pathless git commit commits the entire index under the integrity message after staging the log. Limit the diff/commit to the integrity paths (and the config snapshot, if added) or abort when unrelated staged changes already exist; the restore path uses the same pathless commit pattern too.

Useful? React with 👍 / 👎.

Comment on lines +69 to +79
candidates = [
Path.home() / ".hermes" / "skills" / "devops" / "config-integrity-watchdog",
Path(__file__).parent.parent / "skills" / "devops" / "config-integrity-watchdog",
]
for skills_root in candidates:
if skills_root.exists():
if str(skills_root) not in sys.path:
sys.path.insert(0, str(skills_root))
try:
import config_integrity # noqa: PLC0415
return config_integrity

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Load bundled integrity code before user copies

The CLI trust boundary is weakened because hermes config verify/restore imports config_integrity.py from the mutable user skill directory before the repo-bundled copy. In the threat model described by this feature, a process that can edit config.yaml can also replace this user skill and make verification report OK or run arbitrary code, so the built-in command should prefer trusted bundled code rather than ~/.hermes/skills.

Useful? React with 👍 / 👎.

print(f"Sealed: {config_hash[:16]}...")
print(f" Log: {log_path}")

return 0

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Fail seal when a git commit cannot be anchored

When dotfiles_dir is a git repo but git commit fails, such as missing git identity or a rejecting hook, this still returns success after writing an uncommitted seal entry. The very next verify then returns 2 because the log is dirty, so callers believe they sealed a usable baseline when they actually created an unusable one.

Useful? React with 👍 / 👎.



def _default_dotfiles_dir() -> Path:
return Path(os.environ.get("HERMES_DOTFILES_DIR", "~/Dev/dotfiles")).expanduser()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Configure dotfiles path through Hermes config

This path setting is only read from an env var and otherwise defaults to the contributor-specific ~/Dev/dotfiles; users running the new built-in hermes config seal without that private layout get a mutable fallback under a newly created directory instead of the advertised git-backed anchor. Per the repo config policy, non-secret paths should live in config.yaml or skill metadata rather than relying on ad-hoc env defaults.

Useful? React with 👍 / 👎.

Comment on lines +25 to +28
```bash
# Seal the current config as canonical
python3 ~/.hermes/skills/devops/config-integrity-watchdog/scripts/seal.py

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Use Hermes tool names in the skill run instructions

The root AGENTS.md skill standards require SKILL.md prose to present native Hermes tools or expected MCP servers as the interaction surface, but this How to Run section leads with raw python3 script invocations. Rephrase this around the new hermes config seal/verify/restore commands or the relevant Hermes tools so the skill meets the documented hardline checklist.

Useful? React with 👍 / 👎.

github-actions Bot pushed a commit that referenced this pull request Jul 2, 2026
… mark arc complete (#76)

Session close-out audit found the Outcome section only mentioned PR #67.
PR #69 explicitly self-describes as "follow-up to #67" (unsigned-commit
git fallback) and PR #68 (AGENTS.md docs) is also a direct follow-up;
neither was recorded. Also clarifies that #71/#72 (cron ticker
heartbeat/stall fix) are an unrelated arc shipped the same day, not
part of this project.


Claude-Session: https://claude.ai/code/session_01PQKCc5mDedYAiCNyXnTezh

Co-authored-by: Claude <noreply@anthropic.com>
dizhaky added a commit that referenced this pull request Jul 7, 2026
…writes (#82)

save_config()/restore_config() already keep the local .sha256 sidecar in
sync (PR #57), but the *external* git-backed baseline used by
`hermes config verify` (PR #67) was only ever updated by an explicit
`hermes config seal`. Any authorized write through save_config() — model
scanner, /model command, platform setup flows via
write_platform_config_field() — desynced that baseline, so the Config
Integrity Watchdog cron job flagged the legitimate change as tampering:
"Hermes Config Integrity Failure! The configuration hash does not match
the sealed baseline." This is the same scanner/watchdog TOCTOU conflict
documented in docs/plans/2026-07-02-scanner-watchdog-conflict-resolution.md,
just recurring one layer over in the newer git-backed mechanism.

_write_config_to_disk() now also calls the config-integrity-watchdog
skill's seal() (quietly) whenever $HERMES_DOTFILES_DIR is configured,
keeping the git-backed log current on every authorized write. Extracted
_find_core_module() (returns None instead of exiting) so this can
opportunistically no-op on machines without the watchdog set up.
restore_quick_snapshot() in backup.py gets the same treatment for
config.yaml restores from a quick snapshot.

Co-authored-by: Test <test@test.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants