Skip to content
This repository was archived by the owner on Sep 8, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions .plans/config-integrity-watchdog.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Config Integrity Watchdog — Implementation Plan

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

## Problem

The Config Integrity Watchdog has triggered 25+ times in 19 days. Root cause: the `.sha256` sidecar file is mutable — any process that writes `config.yaml` can also overwrite the fingerprint, masking tampering.

## Solution

Replace mutable sidecar with git-backed append-only integrity log stored in the dotfiles repository.

## Issues (Linear not available — tracked here)

| # | Title | Status |
|---|---|---|
| 1 | Create config-integrity-watchdog skill scaffold | Done |
| 2 | Implement seal.py | Done |
| 3 | Implement verify.py | Done |
| 4 | Implement restore.py | Done |
| 5 | Write tests | Done |
| 6 | Open PR | Done |
| 7 | Add hermes config seal/verify/restore CLI commands | Done |
| 8 | Write CLI integration tests | Done |

## Assumptions

- Dotfiles git repo is at `~/Dev/dotfiles` (configurable via `HERMES_DOTFILES_DIR`)
- `config.yaml` may be a symlink; scripts follow symlinks for hashing
- Canonical model decision (deepseek-v4-pro vs Nemotron-free) deferred — restore.py uses whatever is in the sealed baseline, not a hardcoded model

## Out of scope

- Changing the canonical model ID (requires user decision)
- Config integrity for non-config files
- 1Password integration (future enhancement)
18 changes: 18 additions & 0 deletions hermes_cli/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -5655,6 +5655,21 @@ def config_command(args):

print()

elif subcmd == "seal":
from hermes_cli.config_integrity_cli import cmd_seal
rc = cmd_seal(args)
sys.exit(rc)

elif subcmd == "verify":
from hermes_cli.config_integrity_cli import cmd_verify
rc = cmd_verify(args)
sys.exit(rc)

elif subcmd == "restore":
from hermes_cli.config_integrity_cli import cmd_restore
rc = cmd_restore(args)
sys.exit(rc)

else:
print(f"Unknown config command: {subcmd}")
print()
Expand All @@ -5666,6 +5681,9 @@ def config_command(args):
print(" hermes config migrate Update config with new options")
print(" hermes config path Show config file path")
print(" hermes config env-path Show .env file path")
print(" hermes config seal Hash config.yaml into integrity log")
print(" hermes config verify Check config.yaml against sealed baseline")
print(" hermes config restore Revert config.yaml to sealed baseline")
sys.exit(1)


Expand Down
104 changes: 104 additions & 0 deletions hermes_cli/config_integrity_cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
"""CLI handlers for ``hermes config seal/verify/restore``.

Integrates the config-integrity-watchdog skill into the Hermes CLI.
Subcommands:
seal — hash config.yaml and append a signed entry to the git-committed
integrity log, creating a tamper-evident anchor.
verify — check the current hash against the sealed baseline; exit 1 if
the config has been tampered with.
restore — revert config.yaml to the sealed baseline if it has been tampered.

Exit codes for verify:
0 — config matches canonical baseline
1 — config has been tampered
2 — integrity log itself has uncommitted changes (log tampering)
3 — no baseline found (run seal first)
"""
from __future__ import annotations

import argparse
import sys
from pathlib import Path


# ---------------------------------------------------------------------------
# Register subcommands on an existing config subparsers object
# ---------------------------------------------------------------------------


def register_subcommands(config_subparsers: argparse.Action) -> None:
"""Attach seal/verify/restore parsers to the ``hermes config`` subparser.

Called from ``hermes_cli.main`` after the base config subparsers are set up.
"""
config_subparsers.add_parser(
"seal",
help="Hash config.yaml and anchor it in the git-backed integrity log",
)

config_subparsers.add_parser(
"verify",
help=(
"Check current config.yaml against sealed baseline; "
"exits 1 if tampered"
),
)

config_subparsers.add_parser(
"restore",
help="Revert config.yaml to the sealed git baseline if tampered",
)


# ---------------------------------------------------------------------------
# Handlers — called from hermes_cli.config.config_command dispatch
# ---------------------------------------------------------------------------


def _import_core():
"""Import the shared core module from the skill scripts directory.

We add the skill directory to sys.path on first use rather than at
module-import time so that the import stays lazy (fast startup) and
doesn't conflict with any top-level package names.

Search order:
1. ``~/.hermes/skills/devops/config-integrity-watchdog`` (post-sync location)
2. Repo-relative ``skills/devops/config-integrity-watchdog`` (pre-sync / dev)
"""
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
Comment thread
dizhaky marked this conversation as resolved.
except ImportError:
continue
print(
"ERROR: config-integrity-watchdog skill not found. "
"Run 'hermes skills sync' first."
)
sys.exit(1)


def cmd_seal(args: argparse.Namespace) -> int:
"""Handle ``hermes config seal``."""
core = _import_core()
return core.seal()


def cmd_verify(args: argparse.Namespace) -> int:
"""Handle ``hermes config verify``."""
core = _import_core()
return core.verify()


def cmd_restore(args: argparse.Namespace) -> int:
"""Handle ``hermes config restore``."""
core = _import_core()
return core.restore()
4 changes: 4 additions & 0 deletions hermes_cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -12256,6 +12256,10 @@ def _dispatch_secrets(args): # noqa: ANN001
# config migrate
config_subparsers.add_parser("migrate", help="Update config with new options")

# config integrity commands (seal / verify / restore)
from hermes_cli.config_integrity_cli import register_subcommands as _register_integrity
_register_integrity(config_subparsers)

config_parser.set_defaults(func=cmd_config)

# =========================================================================
Expand Down
50 changes: 50 additions & 0 deletions skills/devops/config-integrity-watchdog/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# config-integrity-watchdog

A tamper-evident integrity system for `~/.hermes/config.yaml` using git-backed fingerprinting.

## After Install

Hermes automatically syncs this skill to `~/.hermes/skills/devops/config-integrity-watchdog/` on next startup.

**Initial seal** (run once after install to establish baseline):
```bash
python3 ~/.hermes/skills/devops/config-integrity-watchdog/scripts/seal.py
```

## Why git-backed?

The existing `.sha256` sidecar file is mutable — any process that can write `config.yaml` can also overwrite the sidecar, masking the tampering. By committing the integrity log to the dotfiles git repository, the fingerprint gains the tamper-evidence of git history: a malicious process without git commit credentials cannot silently forge an entry.

## Configuration

| Env var | Default | Description |
|---|---|---|
| `HERMES_CONFIG` | `~/.hermes/config.yaml` | Path to the config file to protect |
| `HERMES_DOTFILES_DIR` | `~/Dev/dotfiles` | Path to the dotfiles git repository |

## Cron job setup

Replace or augment the existing Config Integrity Watchdog cron job:

**verify** (runs every hour):
```bash
python3 ~/.hermes/skills/devops/config-integrity-watchdog/scripts/verify.py
```

**restore** (runs on verify failure):
```bash
python3 ~/.hermes/skills/devops/config-integrity-watchdog/scripts/restore.py
```

## Graceful fallback

If the dotfiles directory is not a git repository, `seal.py` still writes the log file but skips the git commit and prints a warning. Verification still works (hash comparison), but the log itself is not tamper-evident in that mode.

## Exit codes

| Code | Meaning |
|---|---|
| 0 | OK |
| 1 | Tampered or error |
| 2 | Log file has uncommitted changes (log tampering) |
| 3 | No baseline sealed yet |
49 changes: 49 additions & 0 deletions skills/devops/config-integrity-watchdog/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
---
name: config-integrity-watchdog
description: Detects and restores tampered Hermes config via git log.
version: 1.0.0
author: dizhaky
platforms: [linux, macos]
metadata:
hermes:
tags: [devops, security, config, integrity, watchdog]
related_skills: [ugw-health-check]
---

## When to Use
Comment thread
dizhaky marked this conversation as resolved.

Use this skill when you need to detect or recover from unauthorized changes to `~/.hermes/config.yaml`. The watchdog stores a tamper-evident fingerprint in the dotfiles git repository — unlike a mutable `.sha256` sidecar, a git commit cannot be silently overwritten.

## Prerequisites

- `~/.hermes/config.yaml` must exist (symlink or real file)
- The dotfiles directory must be a git repository (configurable via `HERMES_DOTFILES_DIR`, defaults to `~/Dev/dotfiles`)
- Python 3.9+, no third-party dependencies

## How to Run

```bash
# Seal the current config as canonical
python3 ~/.hermes/skills/devops/config-integrity-watchdog/scripts/seal.py

Comment thread
dizhaky marked this conversation as resolved.
# Verify config integrity
python3 ~/.hermes/skills/devops/config-integrity-watchdog/scripts/verify.py

# Restore canonical config if tampered
python3 ~/.hermes/skills/devops/config-integrity-watchdog/scripts/restore.py
```

## Quick Reference

| Exit code | Meaning |
|---|---|
| 0 | Config matches canonical baseline |
| 1 | Config has been tampered |
| 2 | Integrity log itself has been modified (log tampering) |
| 3 | No baseline found (run seal.py first) |

## Procedure

1. After any intentional config change, run `seal.py` to commit the new baseline.
2. Schedule `verify.py` as a cron job to detect tampering.
3. If tampering is detected, run `restore.py` to revert and re-seal.
Loading
Loading