Skip to main content

ocas-custodian

Automates monitoring and maintenance of agent platforms, fixing operational failures and optimizing system health during quiet hours.

Install this skill

or
0/100

Security score

The ocas-custodian skill was audited on Aug 8, 2026 and we found 12 security issues across 3 threat categories, including 4 high-severity. Review the findings below before installing.

Categories Tested

Security Issues

medium line 71

Access to home directory dotfiles

SourceSKILL.md
69- **STALE-ERROR REFINEMENT (2026-07-22 light scan):** Before applying the destructive `git reset --hard`, FIRST check whether the repo is still stuck — inspect for `.git/rebase-merge`, `.git/rebase-apply`, or `MERGE_HEAD`. If all absent AND `HEAD == origin/main` (working tree clean or only untracked files), the rebase already cleared on its own (a sibling pass or `custodian:update` finished it) and the stored `last_error` is **stale** — do NOT run `git reset --hard` (it can discard legitimate local edits); just re-run the wrapper (`bash <profile>/scripts/update_<skill>.sh <skill>`); if it returns `OK`, the error is resolved. Confirmed: in the 2026-07-22 batch all 18 `:update` repos showed `stuck=[]` with `HEAD==origin/main`; re-running every wrapper succeeded, no reset needed. For the GitHub-LFS push failure variant of backup jobs, see `references/backup-github-lfs-push-failure.md`. Repo-path mapping: a `:update` job named `vesper:update` maps to repo `skills/ocas-vesper` (the `ocas-` prefix), NOT `skills/vesper` — a scan loop that checks `skills/<name>` will report `NO .git` and miss the real repo. Confirmed 2026-07-22: an initial loop used the bare name and falsely reported 15 missing repos; correcting to `ocas-<name>` found all 15 at `HEAD==origin/main`, so the stored `last_error` was stale and `git reset --hard` was correctly avoided.
70
71- **Skill update wrapper path mismatch**: A wrapper may call a legacy hardcoded helper that no longer exists after environment refactors (`python3 ~/.hermes/scripts/skill_update.py ...` → file missing). The modern canonical wrapper is `bash ~/.hermes/profiles/<profile>/scripts/update_skill.sh <skill>`. When an update fail still shows “Script exited with code 1” after repo sync, read the wrapper directly; if it delegates to a missing target, rewrite the wrapper to the canonical helper, then rerun. See `references/skill-update-wrapper-path-mismatch-pattern.md`. Confirmed 2026-07-22.
72
73### Escalation-Runner Cron-Mode JSONL Workflow
medium line 107

Access to home directory dotfiles

SourceSKILL.md
105- `custodian.escalation-runner` -- process escalated Tier 3+ issues
106- `custodian.update` -- self-update from GitHub
107- `custodian.secrets.audit` -- scan configs/skills/scripts/plugins for inline plaintext secrets (API keys, tokens, client secrets, passwords) stored in the "wrong places" instead of the canonical store (`~/.hermes/profiles/<profile>/.env` loaded into `os.environ` at gateway start; `secrets.bitwarden.access_token_env`; MCP `headers` `${ENV}` indirection; `security.redact_secrets`). Read-only. De-dupes by secret value. See `references/secret-audit.md`.
108- `custodian.secrets.remediate` -- plan (and with `--apply`, perform the safe subset of) the migration: move inline MCP `headers` to `${ENV}` indirection, never overwrite an existing `.env` key, back up every touched file. Credential-blob `.json` files and hardcoded `.py` literals are flagged as MANUAL steps (refactor to read `os.getenv`), not blind-edited. Re-run audit to confirm 0 inline hits. See `references/secret-audit.md`.
109
medium line 122

Access to home directory dotfiles

SourceSKILL.md
120**Light Scan** (every heartbeat): Run the following checklist in order. Do not skip steps — each gates the next.
121
122- [ ] 1. Read `jobs.json` (the profile cron registry at `~/.hermes/profiles/<profile>/cron/jobs.json`; NOT via `hermes cron list`, which reads the wrong path). **Parse robustly**: the registry is a top-level object whose job list lives under the key `"jobs"` (a list) — use `d.get("jobs", [])`. A few older/copied registry copies *do* wrap the list under `data.jobs`; only fall back to `d.get("data", {}).get("jobs", [])` if the top-level `"jobs"` key is **absent**. **CRITICAL FALSE-CLEAN GUARD**: if your parse yields `len(jobs) == 0`, re-inspect the raw file head (`head -c 600 jobs.json`) before concluding anything — a wrong key silently returns 0 jobs and risks a false `[SILENT]` on a misparse. Confirmed 2026-07-16: a parse using `data.get("jobs")` returned 0 jobs on a live registry that actually holds 148 (top-level `"jobs"`); the false-clean risk was caught only by re-inspecting the raw file.
123- [ ] 2. Tail gateway log for new errors since last scan timestamp
124 - **CRITICAL GATEWAY-TRACEBACK GAP (2026-07-22):** A jobs.json-only scan can report "clean" (0 actionable error jobs) while the gateway is throwing **recurring plugin-code tracebacks** that NEVER surface as a `jobs.json` `last_error`. These come from gateway-internal paths (e.g. `conversation_compression.py`, `chronicle/engine/store.py`), not from cron job scripts. Example signatures found 2026-07-22: `sqlite3.IntegrityError: CHECK constraint failed: actor IN (...)` in `chronicle/engine/store.py:append_event`, and `TypeError: ChronicleContextEngine.compress() got an unexpected keyword argument 'force'`. Both recurred 14–19× pre-restart, 0× post-restart. Detection procedure:
medium line 126

Access to home directory dotfiles

SourceSKILL.md
124 - **CRITICAL GATEWAY-TRACEBACK GAP (2026-07-22):** A jobs.json-only scan can report "clean" (0 actionable error jobs) while the gateway is throwing **recurring plugin-code tracebacks** that NEVER surface as a `jobs.json` `last_error`. These come from gateway-internal paths (e.g. `conversation_compression.py`, `chronicle/engine/store.py`), not from cron job scripts. Example signatures found 2026-07-22: `sqlite3.IntegrityError: CHECK constraint failed: actor IN (...)` in `chronicle/engine/store.py:append_event`, and `TypeError: ChronicleContextEngine.compress() got an unexpected keyword argument 'force'`. Both recurred 14–19× pre-restart, 0× post-restart. Detection procedure:
125 - `grep -nE "Traceback|IntegrityError|TypeError|ERROR gateway" <gateway.log>` since the last gateway restart line (`Received SIGTERM` / `Starting Hermes Gateway`).
126 - **WHICH `gateway.log` IS LIVE (2026-07-24):** there are TWO. `~/.hermes/logs/gateway.log` (root) is a **stale copy** (last entries June 2026 in this deployment) — grepping it returns pre-restart noise and MISSES live July tracebacks. The **live** log is `~/.hermes/profiles/<profile>/logs/gateway.log` (the one `verify_plugin_defect_postrestart.py` reads); plugin tracebacks also land in `~/.hermes/logs/errors.log`. Verify recency (`grep -oE "20[0-9]{2}-[0-9]{2}-[0-9]{2}" <file> | tail -3`) before trusting any `gateway.log`, and run the post-restart verifier against the LIVE path. Full recipe + confirmed case: `references/gateway-log-live-vs-stale-path-2026-07-24.md`.
127 - Dedup signatures (exception class + in-plugin frame, e.g. `store.py:append_event` / `compress() got ... 'force'`), collect first/last timestamps.
128 - Drop signatures already represented by an OPEN issue in `issues.jsonl` (grep fingerprint/issue_id) — don't double-track.
medium line 146

Access to home directory dotfiles

SourceSKILL.md
144- **Never-run job (`last_status=None` AND `last_run_at=None`):** NOT a stuck-scheduler failure if `next_run_at` is still in the future — the job has simply never been due. Only flag as "not running" when `next_run_at` is ALSO past. Convert `next_run_at` to UTC before comparing (it carries an offset, e.g. `-07:00`; see `references/jobs-json-timestamp-offset-misread-pitfall.md`). Confirmed 2026-07-22: `skill-sync-all` showed `last_status=None` / `last_run_at=None` with `next_run_at=2026-07-23T04:00-07:00` (next day) — correctly left unflagged.
145- [ ] 8. For each fingerprint with `recurrence_count >= 2`, check `rca.jsonl` — if no RCA record exists, flag for deep scan RCA step; if Pattern B, skip fix and note in journal
146- **8b. Journal-to-issues gap check**: for any previous journal entry with `escalation_needed: true`, verify a matching entry exists in `issues.jsonl` for the same fingerprint. If NOT found, write it — the prior scan flagged but failed to persist. (Confirmed pattern: 10:05 scan wrote `escalation_needed: true` but did NOT write the issue to issues.jsonl; 12:04 scan had to write it manually.) **STALE-PREMISE GUARD (2026-07-14):** before writing a gap issue, VERIFY THE LIVE PREMISE is still true — re-scan `jobs.json`, disk usage, and provider state. A journal flagged `escalation_needed: true` can carry a premise that resolved AFTER the journal was written; persisting it creates a FALSE escalation (pollutes `issues.jsonl` and burns an execution-loop cycle). Concrete checks: for `oc_state_db_oversized` re-derive disk% live (`shutil.disk_usage('/root')`) — threshold is db>1GB AND disk>80%; if disk is now `<80%` (even at 5-10GB db) it is acceptable operational cost, do NOT persist. **PATH TRAP (confirmed 2026-07-25):** the canonical state.db is `~/.hermes/profiles/<profile>/state.db` (~14.7GB in production) — the root `~/.hermes/state.db` is a 38-byte placeholder and will make you wrongly conclude the oversized premise is FALSE. Measure the PROFILE path (`ls -la ~/.hermes/profiles/<profile>/state.db` plus `find ~/.hermes -name '*.db' -size +1G` to locate all >1GB DBs) before applying the stale-premise guard. A scan that checks only the root path almost failed to persist a genuine 84.9%-disk / 14.7GB-DB oversized condition. For any `*_access_token_missing` / auth fingerprint, if the implicated job is `status=ok` with cleared `last_error`, it recovered — do NOT persist. For every fingerprint, require ≥1 live job still matching the signature before writing. Confirmed 2026-07-14: `oc_state_db_oversized` (journal claimed disk 82%) was NOT persisted because live disk was 70.2%; `oc_google_tasks_access_token_missing` was NOT persisted because `monitor:list` was already `status=ok`. See `references/journal-escalation-stale-premise-guard-2026-07-14.md`.
147- **8b-variant — 'tracked' in narrative but `escalation_needed: false`**: A scan may reference root-cause fingerprints as 'tracked' (in `previous_scan_delta.stable_root_cause` or prose) while setting `escalation_needed: false` on the journal. If the referenced fingerprint is absent from `issues.jsonl`, the escalation silently dropped — every later scan re-reports it as 'tracked' without ever persisting it. Fix: after classifying non-auto-fixable root causes, collect their intended fingerprints and verify each exists in `issues.jsonl` (use `scripts/parse_issues_jsonl.py`); if missing, WRITE it (`status: open`, `escalation_needed: true`). One issue per root-cause fingerprint; list affected job names in `affected_components`. Confirmed 2026-07-07: 71-job 402-credits and 2-job OAuth revocations were reported 'tracked' in prior deltas but absent from `issues.jsonl`; light scan wrote them. See `references/escalation-persistence-gap.md`.
148- **8b/8e parser gotcha (confirmed 2026-07-14):** When reading `issues.jsonl` for gap checks, a hand-written brace-walk parser inside `python3 << 'PYEOF'` that does quote/backslash tracking returns **0 objects** (its escape handling breaks on `\"`). Use `json.JSONDecoder().raw_decode` instead — robust to escaped quotes and concat-per-line objects. In concurrent cron contexts (sibling `custodian:light` rewrites the file at top of hour) re-verify the file `mtime`/`size` across 3 quick reads before trusting a parse; prefer `grep -ac` for a single targeted check (e.g. does this `issue_id` already exist) to avoid a full race-prone rewrite. Race-safe recipe in `references/monitor-list-exit1-mask-gap.md`.
medium line 150

Access to home directory dotfiles

SourceSKILL.md
148- **8b/8e parser gotcha (confirmed 2026-07-14):** When reading `issues.jsonl` for gap checks, a hand-written brace-walk parser inside `python3 << 'PYEOF'` that does quote/backslash tracking returns **0 objects** (its escape handling breaks on `\"`). Use `json.JSONDecoder().raw_decode` instead — robust to escaped quotes and concat-per-line objects. In concurrent cron contexts (sibling `custodian:light` rewrites the file at top of hour) re-verify the file `mtime`/`size` across 3 quick reads before trusting a parse; prefer `grep -ac` for a single targeted check (e.g. does this `issue_id` already exist) to avoid a full race-prone rewrite. Race-safe recipe in `references/monitor-list-exit1-mask-gap.md`.
149- [ ] 8f. **Recurrence-of-fingerprint-CLASS across distinct jobs (gap Step 8b/8b-variant does NOT cover):** The journal→issues gap checks only fire when a journal *flagged* `escalation_needed: true` but no open issue exists. They do NOT catch a live error job whose **fingerprint class** was seen before but whose prior issue was correctly *resolved per-job* (not per-fingerprint-family). Consequence: a same-class error silently recurs on a NEW job with no open issue and no flag, and every scan re-classifies it as "pre-classified" / "known pattern" without persisting anything. **Procedure:** for each live error job (Steps 6–8), map its error to a reusable fingerprint *class* (e.g. `content_policy_blocked`, `token_expired`, `402 credits`, `interpreter shutdown`, `Script exited with code 1`). Grep the full `issues.jsonl` (resolved + open) for that class token. If the class was previously seen but ALL matching issues are now `status: resolved`/`duplicate` AND the *current* job is **not** listed in any of those issues' `affected_job_ids` → this is a NEW occurrence of a recurring class on a distinct job → **persist a fresh issue** (`status: open`, `escalation_needed: true`, `user_gated` per class nature, `affected_job_ids` = [current job id]) even though no journal flagged it. Do NOT re-open the old resolved issue (it was correct for its own job); write a new dated issue. STALE-PREMISE GUARD still applies: require ≥1 live enabled job currently matching the signature before writing. Confirmed 2026-07-23: `ocas-autobio-observe` errored live with `content_policy_blocked`; prior same-class issues `oc_bones_content_policy_blocked_20260720` and `oc_sands_evening_brief_content_policy_20260722` were both `resolved` (Mentor note "re-escalate if recurrence detected") and listed only their own jobs — no open issue covered `ocas-autobio-observe`, so a new issue `oc_autobio_content_policy_blocked_20260723T0505Z` was written. See `references/recurrence-fingerprint-class-distinct-job-2026-07-23.md`.
150- [ ] 8c. **Verify-before-accepting-self-resolved**: when a prior scan classified an error as "self-resolved" (e.g., `ModuleNotFoundError` that supposedly fixed itself), verify by running the actual import in the cron execution python — NOT any assumed venv path. Cron jobs run `python3` from PATH. To find the actual python: `which python3` in a terminal, then `python3 -c "import <module>"`. The profile venv path (`~/.hermes/profiles/<profile>/venv/bin/python3`) may NOT exist — the system hermes venv (`<hermes-venv>/bin/python3`) is typically the active one. Confirmed 2026-07-01: `dispatch:triage-morning` was classified "self-resolved" but the verification was done by checking the actual import (import google.oauth2.credentials → OK). Do NOT accept "self-resolved" from a prior journal entry without re-verifying when: (a) the module path in the error differs from what you assumed, (b) the error was from a no_agent script (different python resolution), or (c) the prior scan has known counting discrepancies. See `references/self-resolved-module-verification-pattern.md`.
151- [ ] 8d. **Verify-before-accepting-prior-resolution (inverse gotcha for light scans)**: A prior scan may have marked a `user_gated`/`resolved` issue `resolved` on a "provider recovered / forward-stale" theory derived from an OLD `last_run_at` — WITHOUT checking whether live jobs still error. `last_run_at` age is NOT proof of recovery (an old `last_run_at` just means the job hasn't re-run). Before accepting any prior `resolved` classification for a provider/auth/credit fingerprint, re-scan the live `jobs.json`: count enabled jobs with `last_status=error` AND a current `last_error` still matching that outage signature (`token_expired`, `402 ... credits`, `No endpoints found for ...owl-alpha`). If ≥1 exists, the resolution was FALSE — reopen the issue (`status: user_gated`, `escalation_needed: true`, clear `resolved_at`, set `reopened_at` + `reopen_note` citing the live count). Only a CLEARED `last_error` + `last_status=ok` on a post-fix run proves recovery. Automate with `scripts/reopen_false_resolutions.py` (dry-run; `--write` to persist). Confirmed 2026-07-13: a 16:10 light scan marked the Nous-401 (`token_expired`) + OpenRouter-402 (credits) outage issues `resolved` (reason "forward-stale provider recovered", from `last_run_at` 2026-07-12), but the 19:00 scan found 18 jobs STILL erroring live with `token_expired`/402 and no re-auth evidence — reopened 2 issues. See `references/light-scan-false-resolution-gotcha.md`.
152- [ ] 8e. **Verify resolved CODE-DEFECT fixes actually cover all references** (catches what `reopen_false_resolutions.py` misses): that script only matches provider/auth/credit outage signatures (`token_expired`, `402 credits`, `owl-alpha 404`) — it does NOT catch a `resolved` issue whose fingerprint is a code defect (`oc_*_bug`, `oc_*_missing`, `oc_*_path_*`) whose fix was incomplete. For each such `resolved` issue: (a) does any enabled job's `last_error` STILL contain the original error signature? If yes, first compare the job's `last_run_at` against the fix timestamp — if the job ran BEFORE the fix landed, the error is STALE (re-run the script to confirm), not a live regression; (b) does the source file the fix touched STILL contain the broken reference the error named? A "resolved" entry whose fix only added a comment saying 'we now use X' while the erroring line still references old Y is a FALSE resolution — reopen (`status: open`, `escalation_needed: true`, `tier: 4`, clear `resolved_at`, set `reopened_at` + `reopen_note`). Grep recipe: `grep -rn "<broken_token>" <file>`. Re-run the script directly (`<hermes-venv>/bin/python <script>` or `bash <wrapper>`) and inspect exit code + stderr to separate stale from live. **CORRECTED 2026-07-13 (supersedes the original misread assertion):** The original reopen directive for `oc_chronicle_facts_fts_missing_20260713` was a MISREAD and must NOT be followed. It claimed `enrich_embeddings.py:121` still executed `DELETE FROM facts_fts`, but line 121 is a COMMENT; the executable statements (lines 130/149) use the live `belief_fts`/`observed_fts` tables, which exist in `chronicle.db` and rebuild cleanly (verified by dry-executing both DELETEs against the live DB — no `OperationalError`). The job's stored error was STALE: last ran 2026-07-13 10:02 UTC, BEFORE the fix landed (file mtime 11:36 UTC). The resolution was CORRECT — leave it resolved. **Grep-pitfall (root cause of the misread):** `grep -rn "facts_fts" enrich_embeddings.py` matches only the historical comment lines (120, 124), producing a false "still broken" hit. A token appearing in comments is NOT proof the broken code path runs — read the actual executable lines around the cited line number, or execute the real code path, before reopening. Always compare `last_run_at` vs the fix file's mtime: an error from a run BEFORE the fix is stale, not a live regression. **FALSE-CLOSE via drained-backlog test (inverse, confirmed 2026-07-13):** When a prior scan *resolved* a timeout/volume code-defect issue on the strength of a live re-run that completed fast, verify that re-run executed against the REAL production backlog — NOT a queue just drained by a prior run. A timeout issue can finish in ~163s when there is nothing left to embed, then HANG past the 600s cron hard limit once daily volume rebuilds. In this session `oc_script_timeout_chronicle_embed_20260713` was closed at 21:36Z citing a 163s pass, but its `embed_state.json` showed `last_run=21:35:56` (a prior run had just drained the queue), and a fresh re-run at 23:0xZ was still actively embedding at 85s (row 4160/8000 of the facts pass; `facts` table holds 35,486 rows) — confirming the timeout is recurring. Before accepting a timeout/throughput 'resolved': (1) check the progress/state file's `last_run` vs the claimed fix time — if within seconds, the test ran on a cleared queue; (2) inspect real data volume (`SELECT COUNT(*) FROM facts`); (3) re-run the actual script against that full volume with a hard cap (background `terminal(background=true, notify_on_complete=true)` + `process(wait/poll)` — foreground cap is 60s) and confirm it completes under the cron limit. A clean run immediately after another successful run proves nothing about steady-state load. See `references/resolved-codefix-regression-verify.md` and `references/resolved-timeout-verify-drained-backlog.md`.
medium line 332

Access to home directory dotfiles

SourceSKILL.md
330### Plugin vs Skill Architecture
331
332The **plugin** (`~/.hermes/plugins/custodian/`) is the active code loaded by the gateway. The **skill** (`~/.hermes/profiles/<profile>/skills/ocas-custodian/`) is a reference copy. Do not recreate as standalone.
333
334- **Actual update:** `cd ~/.hermes/plugins/custodian && git pull`
medium line 334

Access to home directory dotfiles

SourceSKILL.md
332The **plugin** (`~/.hermes/plugins/custodian/`) is the active code loaded by the gateway. The **skill** (`~/.hermes/profiles/<profile>/skills/ocas-custodian/`) is a reference copy. Do not recreate as standalone.
333
334- **Actual update:** `cd ~/.hermes/plugins/custodian && git pull`
335- **Version:** Check plugin `__init__.py` `__version__` or `git log -1 --oneline`
336- See `references/plugin-vs-skill-architecture.md` for editable install details
high line 198

Windows registry startup

SourceSKILL.md
1963. **Classify** into one of:
197 - **Actionable** (Tier 1 auto-fix per the fix-safety envelope).
198 - **Code-defect fixable by THIS loop** — a third-party SDK bug, skill-owned script defect, or env/version mismatch the agent can correct (NOT in the Tier-1 registry, NOT user-gated, but still resolvable here). Look for: a `pydantic`/`ValidationError` on a third-party package, a documented patch in the skill's `references/`, or a version mismatch. **Verify the fix by running the ACTUAL cron script the job invokes**, in the exact env its wrapper uses — do NOT trust a version bump alone:
199 `env -i HOME=<hermes-home>/profiles/indigo/home PATH=/usr/local/bin:/usr/bin:/bin HERMES_HOME=<hermes-home>/profiles/indigo <hermes-venv>/bin/python <script>` → expect `EXIT_CODE=0` with real output. If it exits 0, mark `status: resolved`, `escalation_needed: false`, record `fix_applied` + `verified`, and **clear any leftover `user_gated: true`** carried from open-time (it was mislabeled — root cause was code, not credentials).
200 - **User-gated** (credits, API key, skill-internal hardcoded model, revoked OAuth) — needs <operator>. Leave open, no pause (retry policy).
high line 240

Windows registry startup

SourceSKILL.md
238**Contrast with the weak-probe false-resolution gotcha** (in `references/escalation-execution-loop.md`): that one warns against *resolving too eagerly* on a cheap probe; this one warns against *NOT resolving* because you trusted a stale `last_error`. Both hinge on the same principle — the live re-run is the only valid evidence, never the stored error string.
239
240**Post-fix verification**: After applying any Tier 1 auto-fix, re-check the targeted log entry or config state to confirm the error no longer appears. **Close the loop on the registry itself:** `jobs.json` `last_status` only updates when the job next *executes* — so a freshly-fixed job keeps showing stale `error` until its next scheduled run (often a day away). Re-run it on demand with `hermes cron run <id>` to flip the registry to `ok` now and prove the fix held. no_agent jobs return in ~2s and print `Ran now: succeeded.` / `failed.`; a serial foreground loop over ~17 IDs fits the 180s terminal cap. Do NOT use shell `&` backgrounding (blocked by the tirith filter) — use a serial loop or `terminal(background=true)`. Use `scripts/verify_fixes_cron_run.py ID1 ID2 ...` to batch-verify. (Confirmed 2026-07-22: 17 fixed jobs reported `succeeded` via `hermes cron run`; registry dropped 24→7 error jobs.)
241
242**Empty plugin directory detection**: During cron scanning, check for empty plugin directories. See `references/empty-plugin-dir-detection.md`. This is a Tier 2 issue (requires investigation, not auto-fixed). See `references/chronicle-plugin-dirs-empty-pattern.md` for the specific Chronicle plugin case.
high line 376

Windows registry startup

SourceSKILL.md
374| Gateway SIGTERM (exit code 1) | Clean teardown — NOT an error. systemd `Restart=on-failure` revives it. Do NOT escalate |
375| `state.db` >1GB AND disk >80% | Flag as `oc_state_db_oversized` (Tier 2). Recommend message pruning over VACUUM when disk >80% |
376| `jobs.json` `last_status` still `error` after a fix | Registry lags the real fix until the job's next execution. Run `hermes cron run <id>` (no_agent ~2s) to force a run and flip the registry to `ok`; verify with `scripts/verify_fixes_cron_run.py`. Do NOT treat stale `error` as proof the fix failed. Confirmed 2026-07-22. |
377| Command text contains literal substring `gateway restart` (even `echo "...gateway restart..." > file` or a heredoc writing a note) | Sandbox interlock scans the raw command for the phrase and refuses with "Blocked: cannot restart or stop the gateway from inside the gateway process" — even when the command does nothing of the sort. Reword the command to avoid the literal phrase (e.g. "gateway reload" / "reload the Hermes process"); write files with text that omits the trigger token. Confirmed 2026-07-25: an issue-patch heredoc containing the phrase failed 3× (exit 1); a benign write with the phrase removed succeeded (exit 0). See the PLUGIN CODE FIXES section for the broader guard. |
378
high line 152

Urgency-based manipulation

SourceSKILL.md
150- [ ] 8c. **Verify-before-accepting-self-resolved**: when a prior scan classified an error as "self-resolved" (e.g., `ModuleNotFoundError` that supposedly fixed itself), verify by running the actual import in the cron execution python — NOT any assumed venv path. Cron jobs run `python3` from PATH. To find the actual python: `which python3` in a terminal, then `python3 -c "import <module>"`. The profile venv path (`~/.hermes/profiles/<profile>/venv/bin/python3`) may NOT exist — the system hermes venv (`<hermes-venv>/bin/python3`) is typically the active one. Confirmed 2026-07-01: `dispatch:triage-morning` was classified "self-resolved" but the verification was done by checking the actual import (import google.oauth2.credentials → OK). Do NOT accept "self-resolved" from a prior journal entry without re-verifying when: (a) the module path in the error differs from what you assumed, (b) the error was from a no_agent script (different python resolution), or (c) the prior scan has known counting discrepancies. See `references/self-resolved-module-verification-pattern.md`.
151- [ ] 8d. **Verify-before-accepting-prior-resolution (inverse gotcha for light scans)**: A prior scan may have marked a `user_gated`/`resolved` issue `resolved` on a "provider recovered / forward-stale" theory derived from an OLD `last_run_at` — WITHOUT checking whether live jobs still error. `last_run_at` age is NOT proof of recovery (an old `last_run_at` just means the job hasn't re-run). Before accepting any prior `resolved` classification for a provider/auth/credit fingerprint, re-scan the live `jobs.json`: count enabled jobs with `last_status=error` AND a current `last_error` still matching that outage signature (`token_expired`, `402 ... credits`, `No endpoints found for ...owl-alpha`). If ≥1 exists, the resolution was FALSE — reopen the issue (`status: user_gated`, `escalation_needed: true`, clear `resolved_at`, set `reopened_at` + `reopen_note` citing the live count). Only a CLEARED `last_error` + `last_status=ok` on a post-fix run proves recovery. Automate with `scripts/reopen_false_resolutions.py` (dry-run; `--write` to persist). Confirmed 2026-07-13: a 16:10 light scan marked the Nous-401 (`token_expired`) + OpenRouter-402 (credits) outage issues `resolved` (reason "forward-stale provider recovered", from `last_run_at` 2026-07-12), but the 19:00 scan found 18 jobs STILL erroring live with `token_expired`/402 and no re-auth evidence — reopened 2 issues. See `references/light-scan-false-resolution-gotcha.md`.
152- [ ] 8e. **Verify resolved CODE-DEFECT fixes actually cover all references** (catches what `reopen_false_resolutions.py` misses): that script only matches provider/auth/credit outage signatures (`token_expired`, `402 credits`, `owl-alpha 404`) — it does NOT catch a `resolved` issue whose fingerprint is a code defect (`oc_*_bug`, `oc_*_missing`, `oc_*_path_*`) whose fix was incomplete. For each such `resolved` issue: (a) does any enabled job's `last_error` STILL contain the original error signature? If yes, first compare the job's `last_run_at` against the fix timestamp — if the job ran BEFORE the fix landed, the error is STALE (re-run the script to confirm), not a live regression; (b) does the source file the fix touched STILL contain the broken reference the error named? A "resolved" entry whose fix only added a comment saying 'we now use X' while the erroring line still references old Y is a FALSE resolution — reopen (`status: open`, `escalation_needed: true`, `tier: 4`, clear `resolved_at`, set `reopened_at` + `reopen_note`). Grep recipe: `grep -rn "<broken_token>" <file>`. Re-run the script directly (`<hermes-venv>/bin/python <script>` or `bash <wrapper>`) and inspect exit code + stderr to separate stale from live. **CORRECTED 2026-07-13 (supersedes the original misread assertion):** The original reopen directive for `oc_chronicle_facts_fts_missing_20260713` was a MISREAD and must NOT be followed. It claimed `enrich_embeddings.py:121` still executed `DELETE FROM facts_fts`, but line 121 is a COMMENT; the executable statements (lines 130/149) use the live `belief_fts`/`observed_fts` tables, which exist in `chronicle.db` and rebuild cleanly (verified by dry-executing both DELETEs against the live DB — no `OperationalError`). The job's stored error was STALE: last ran 2026-07-13 10:02 UTC, BEFORE the fix landed (file mtime 11:36 UTC). The resolution was CORRECT — leave it resolved. **Grep-pitfall (root cause of the misread):** `grep -rn "facts_fts" enrich_embeddings.py` matches only the historical comment lines (120, 124), producing a false "still broken" hit. A token appearing in comments is NOT proof the broken code path runs — read the actual executable lines around the cited line number, or execute the real code path, before reopening. Always compare `last_run_at` vs the fix file's mtime: an error from a run BEFORE the fix is stale, not a live regression. **FALSE-CLOSE via drained-backlog test (inverse, confirmed 2026-07-13):** When a prior scan *resolved* a timeout/volume code-defect issue on the strength of a live re-run that completed fast, verify that re-run executed against the REAL production backlog — NOT a queue just drained by a prior run. A timeout issue can finish in ~163s when there is nothing left to embed, then HANG past the 600s cron hard limit once daily volume rebuilds. In this session `oc_script_timeout_chronicle_embed_20260713` was closed at 21:36Z citing a 163s pass, but its `embed_state.json` showed `last_run=21:35:56` (a prior run had just drained the queue), and a fresh re-run at 23:0xZ was still actively embedding at 85s (row 4160/8000 of the facts pass; `facts` table holds 35,486 rows) — confirming the timeout is recurring. Before accepting a timeout/throughput 'resolved': (1) check the progress/state file's `last_run` vs the claimed fix time — if within seconds, the test ran on a cleared queue; (2) inspect real data volume (`SELECT COUNT(*) FROM facts`); (3) re-run the actual script against that full volume with a hard cap (background `terminal(background=true, notify_on_complete=true)` + `process(wait/poll)` — foreground cap is 60s) and confirm it completes under the cron limit. A clean run immediately after another successful run proves nothing about steady-state load. See `references/resolved-codefix-regression-verify.md` and `references/resolved-timeout-verify-drained-backlog.md`.
153- [ ] 9. **Verify-before-acting**: for any error job, check current `config.yaml` and provider state to confirm the error is still active before attempting fix
154- [ ] 10. Write observation journal (even if no issues found — set `not_activity_reason`)
Scanned on Aug 8, 2026
View Security Dashboard
Installation guide →