Skip to content

fix(provision,guard,store): pin hooks to a canonical omind, fix title recall - #229

Merged
CryptoJones merged 2 commits into
mainfrom
fix/canonical-hook-paths-and-guard-precision
Aug 11, 2026
Merged

fix(provision,guard,store): pin hooks to a canonical omind, fix title recall#229
CryptoJones merged 2 commits into
mainfrom
fix/canonical-hook-paths-and-guard-precision

Conversation

@CryptoJones

Copy link
Copy Markdown
Owner

Six fixes from an audit of a live install. The new docs/install-verification.md is the matrix that found them: ~70 agent-executable rows checking that a new install's features, controls, and hooks are actually present and behaving — not merely wired.

The root cause

Hook commands baked shutil.which("omind") into settings.json as an absolute path frozen at setup time. On a dev box that captured an editable checkout's venv, so omind self-update upgraded ~/.local/bin while all five hooks kept executing a months-old build — silently, because the wiring still looked correct.

Fixed by pinning to ~/.local/bin/omind. On a uv tool install box that is a symlink uv retargets on every upgrade, which gives both properties the old code was chasing separately: absolute (fires in a shell without ~/.local/bin on PATH) and stable (an update never strands it). Resolution now happens once at install time instead of being frozen into five hook entries. Applied to all 13 which("omind") sites, so hermes/opencode/codex/gemini/openclaw get it too — not just Claude Code — plus a doctor check that fails on a non-canonical pin.

The other five

  • store.safe_name title fallback. The guard names notes by TITLE in block messages, and 73/831 notes on the audited vault have / in their title, so recall-note rejected them for path separators. The guard would block an action, instruct a recall of "NEVER offer to end/pause the session …", and then refuse it — a remediation loop impossible to satisfy. The fallback requires the note to exist and re-runs full validation, so traversal stays impossible and creates still fail loudly.
  • self-update re-provisions and warms the index. Opening the index runs the existing SCHEMA_VERSION/model check, so a format migration is paid during the update the user is already waiting on instead of ambushing the next search. Not an unconditional rebuild — the schema check already decides. Both steps fail-open, opt out with the existing OMIND_NO_AUTOHEAL.
  • guard pause capped at 4h. A box was found paused for 185h with the consult-gate and verifier off; the only visible trace was guard status, which nobody runs unprompted.
  • PAUSED banner in SessionStart priming, so a degraded gate is impossible to miss.
  • doctor reports the deny RATE, warning above 25%. The audited install sat at 51% — one action in two — which the raw counts hid. Precision, not volume, decides whether the guard is helping.

Testing

928 tests pass; ruff clean. New regression tests cover the title fallback (including four traversal attempts and the create path), the canonical resolver and its fallback, _hook_exe_path parsing, and both pause-cap branches.

Both new doctor checks were verified firing against the real install that motivated them.

Proudly Made in Nebraska. Go Big Red! 🌽 https://xkcd.com/2347/

… recall

Six fixes from an audit of a live install (docs/install-verification.md).

Hook commands baked shutil.which("omind") into settings.json as an absolute
path frozen at setup time. On a dev box that captured an editable checkout's
venv, so `omind self-update` upgraded ~/.local/bin while all five hooks kept
executing a months-old build -- silently, because the wiring still looked
correct. Pin to ~/.local/bin/omind instead: on a uv install that symlink is
retargeted on every upgrade, giving an absolute path (fires without
~/.local/bin on PATH) that is also stable (an update never strands it).
Applied to all 13 which("omind") sites so every harness gets the property,
not just Claude Code, plus a doctor check that fails on a non-canonical pin.

store.safe_name now falls back to the sanitized title. The guard names notes
by TITLE in block messages and 73/831 notes have '/' in their title, so
recall-note rejected them for path separators -- the guard demanded a retry
that could not be satisfied. The fallback requires the note to exist and
re-runs full validation, so traversal stays impossible and creates still fail.

Also: self-update re-provisions and warms the index (fail-open, opt out with
OMIND_NO_AUTOHEAL); guard pause capped at 4h after a box was found paused for
185h with enforcement silently off; a PAUSED banner in SessionStart priming;
and doctor reports the compliance deny RATE, warning above 25%.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T3gMTVBvDX84KKe2h8HXvJ
@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@CryptoJones, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 51 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 3c5e6fbc-6cb2-4946-99ae-7b6645e51ae2

📥 Commits

Reviewing files that changed from the base of the PR and between 8b3fa3f and 801db70.

📒 Files selected for processing (2)
  • src/omind/provision.py
  • tests/test_provision.py
📝 Walkthrough

Summary by CodeRabbit

  • New Features
    • Improved installation and integration reliability by consistently locating the correct omind executable.
    • Added diagnostics for outdated hook configurations and compliance deny-rate warnings.
    • Added automatic post-update repair and search-index refresh, with an opt-out setting.
    • Limited operator pauses to four hours and clearly reports capped durations.
    • Added session warnings when consultation safeguards are paused.
    • Improved note-name handling for titles with sanitized filenames while preserving security checks.
  • Documentation
    • Added an installation verification matrix covering setup, safeguards, integrations, recovery, and scheduled features.

Walkthrough

The change standardizes omind executable resolution across provisioning and service integrations. It adds hook drift diagnostics, compliance warnings, bounded guard pauses, paused-session banners, safe note-title lookup, post-update healing, and an installation verification matrix.

Changes

Installation Reliability and Runtime Guardrails

Layer / File(s) Summary
Canonical executable wiring
src/omind/provision.py, src/omind/agents.py, src/omind/backup.py, src/omind/mesh.py, tests/test_agents.py, tests/test_provision.py
canonical_omind_exe() prefers ~/.local/bin/omind and falls back to PATH. Generated hooks, MCP commands, guard scripts, backup services, and mesh services use this resolver. Tests cover canonical-path preference and fallback behavior.
Drift diagnostics and update healing
src/omind/provision.py, src/omind/update.py
Hook diagnosis reports stale executable paths. Compliance diagnostics warn at a 25% deny rate. Successful updates re-provision wiring and refresh the search index unless auto-healing is disabled.
Guard pause and session banners
src/omind/guard.py, src/omind/hooks.py, tests/test_guard.py
Operator pauses are limited to four hours and report capped durations. Session priming reports paused gate state and update notifications.
Safe note-name resolution
src/omind/store.py, tests/test_store.py
safe_name resolves existing notes from sanitized titles while preserving validation and traversal protections.
Installation verification guide
docs/install-verification.md
The guide defines command-based checks for installation, hooks, guard policy, MCP tools, storage, retrieval, synchronization, backups, scheduled features, and final reporting.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant UpdateCommand
  participant Provisioning
  participant SearchIndex
  participant UpdateLogger
  UpdateCommand->>Provisioning: re-run provisioning
  UpdateCommand->>SearchIndex: refresh shared search index
  Provisioning-->>UpdateLogger: log changes or warnings
  SearchIndex-->>UpdateLogger: log refresh or migration results
  UpdateCommand->>UpdateLogger: log restart instruction
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies two primary changes: canonical hook pinning and title-based note recall fixes.
Description check ✅ Passed The description directly explains the audit findings, implemented fixes, regression tests, and verification results.
Docstring Coverage ✅ Passed Docstring coverage is 89.29% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/canonical-hook-paths-and-guard-precision

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Comment thread src/omind/backup.py
from omind.paths import INDEX_FILENAME
from omind.proc import DEFAULT_TIMEOUT, run_command
from omind.provision import CheckResult, Logger, SetupConfig
from omind.provision import CheckResult, Logger, SetupConfig, canonical_omind_exe
Comment thread src/omind/mesh.py
raise MeshError(f"not a mesh node yet — run `omind mesh init` first ({omi_dir})")
omind_exe = shutil.which("omind") or "omind"
# Imported lazily: provision imports mesh, so a module-level import cycles.
from omind.provision import canonical_omind_exe

@coderabbitai coderabbitai 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.

Actionable comments posted: 11

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
docs/install-verification.md (1)

174-177: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Add the required documentation footer.

The file ends without:

*Proudly Made in Nebraska. Go Big Red! 🌽 <https://xkcd.com/2347/>*

Append this exact footer after the final paragraph. The centered banner exception applies only to README.

As per coding guidelines: Documentation files must include the required footer.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/install-verification.md` around lines 174 - 177, Append the exact
required “Proudly Made in Nebraska. Go Big Red!” footer, including the corn
emoji and URL, after the final paragraph in docs/install-verification.md; do not
center it, since the README-only exception does not apply.

Source: Coding guidelines

src/omind/provision.py (1)

603-614: 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Quote the executable at every string-command boundary.

An absolute executable path can contain spaces. The raw path breaks the executable token in the hook, OpenClaw, Gemini, backup, mesh, and checkpoint command strings. Encode it for each target parser. Serialize the Windows schtasks /TR command separately because it contains nested quoting.

Keep executable paths raw in MCP argv arrays. Codex already quotes its executable. Add tests with an executable path that contains spaces.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/omind/provision.py` around lines 603 - 614, Quote the executable path at
every string-command boundary so paths containing spaces remain a single command
token. Update the hook command built by the relevant provision method, the
OpenClaw/Gemini/backup/mesh/checkpoint command builders in src/omind/agents.py
(lines 333-345, 738, and 808-814), src/omind/backup.py (lines 413-420), and
src/omind/mesh.py (lines 891-894); serialize the Windows schtasks /TR command
separately for its nested quoting. Keep executable paths raw in MCP argv arrays,
preserve Codex’s existing quoting, and add tests using an executable path
containing spaces.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/install-verification.md`:
- Line 156: Correct the I4 verification step in the documentation by replacing
the invalid git -C status reference with a valid command such as git -C
"$TESTVAULT/OMI" status --short, and ensure the before-and-after checks compare
the vault state without mutation.
- Line 166: Update the verification summary’s B-group total to account for all
13 defined B checks, changing the example from B: 11/11 to B: 13/13; do not
exclude B7a or B7b.
- Around line 19-23: Add blank lines immediately before and after the shell code
fence in the installation verification instructions, and label the reporting
example fence around the reporting section with the text language. Preserve the
existing command and reporting content unchanged.
- Around line 124-134: Add a retrieval acceptance row in section G that runs a
known-phrase search with OMI_INDEX_DISABLE=1 and verifies the expected note is
returned through the fallback path. If the test harness supports index-error
injection, add coverage for that failure case as well, confirming search still
falls back successfully and preserves the documented fail-open behavior.
- Around line 43-59: Update the verification matrix so B3 and B5 require MCP and
hook commands to resolve to the canonical ~/.local/bin/omind executable, not
merely the command name. Add equivalent wiring checks for every supported
harness exercised by D1: Hermes, OpenCode, Codex, Gemini, and OpenClaw. Reuse
the existing desired_server_entry() comparison behavior from agents.py to detect
stale virtual-environment binaries and non-Claude drift.

In `@src/omind/guard.py`:
- Around line 1578-1584: Update the pause boundary in pause_gate to enforce
_MAX_PAUSE_SECONDS for direct callers, applying the same capped duration before
persisting or starting the pause. If pause_gate is not intended as a supported
API, rename it to _pause_gate and ensure supported callers continue routing
through _run_pause.

In `@src/omind/provision.py`:
- Around line 316-323: Update the hook executable detection around shlex.split
and the token scan to use platform-correct parsing, preserve Windows
backslashes, and only return the executable when its path is absolute rather
than merely containing os.sep. Extend the regression coverage for relative
commands such as ./venv/bin/omind and Windows absolute paths such as
C:\Users\u\Scripts\omind.EXE.

In `@src/omind/store.py`:
- Around line 951-956: Update OmiStore.safe_name around _validated_name and
_name_from_title to reject absolute paths and any input containing a “..” path
segment before title fallback; preserve NoteError for these traversal-shaped
names. In tests/test_store.py lines 902-909, create matching sanitized files
under tmp_path and assert that ../outside and /etc/passwd still raise NoteError.

In `@src/omind/update.py`:
- Around line 226-235: Update the post-update provisioning flow around
Provisioner.run to load the persisted active SetupConfig instead of constructing
one with default_vault_path(). Use the configured vault, folder, and agent
values when re-provisioning; if no saved setup configuration exists, skip
provisioning and emit a warning rather than modifying the defaults.

In `@tests/test_guard.py`:
- Around line 1386-1397: Strengthen test_guard_pause duration assertions by
verifying the effective remaining time is approximately _MAX_PAUSE_SECONDS for
the capped 185h request and approximately 1,800 seconds for the 30m request,
using a small timing tolerance or deterministic clock injection. Keep the
existing cap-output and resume behavior checks intact.
- Around line 1383-1398: Ensure both pause tests always clean up the pause
sentinel by moving guard.resume_gate() into a finally block surrounding each
test’s assertions and output checks. Update test_guard_pause_is_capped and
test_guard_pause_under_the_cap_is_untouched without changing their existing
validation behavior.

---

Outside diff comments:
In `@docs/install-verification.md`:
- Around line 174-177: Append the exact required “Proudly Made in Nebraska. Go
Big Red!” footer, including the corn emoji and URL, after the final paragraph in
docs/install-verification.md; do not center it, since the README-only exception
does not apply.

In `@src/omind/provision.py`:
- Around line 603-614: Quote the executable path at every string-command
boundary so paths containing spaces remain a single command token. Update the
hook command built by the relevant provision method, the
OpenClaw/Gemini/backup/mesh/checkpoint command builders in src/omind/agents.py
(lines 333-345, 738, and 808-814), src/omind/backup.py (lines 413-420), and
src/omind/mesh.py (lines 891-894); serialize the Windows schtasks /TR command
separately for its nested quoting. Keep executable paths raw in MCP argv arrays,
preserve Codex’s existing quoting, and add tests using an executable path
containing spaces.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 085de806-9150-4646-a96b-1892e1095adc

📥 Commits

Reviewing files that changed from the base of the PR and between 9a25738 and 8b3fa3f.

📒 Files selected for processing (13)
  • docs/install-verification.md
  • src/omind/agents.py
  • src/omind/backup.py
  • src/omind/guard.py
  • src/omind/hooks.py
  • src/omind/mesh.py
  • src/omind/provision.py
  • src/omind/store.py
  • src/omind/update.py
  • tests/test_agents.py
  • tests/test_guard.py
  • tests/test_provision.py
  • tests/test_store.py
📜 Review details
⏰ Context from checks skipped due to timeout. (10)
  • GitHub Check: test (ubuntu-latest, 3.12)
  • GitHub Check: test (ubuntu-latest, 3.11)
  • GitHub Check: test (ubuntu-latest, 3.14)
  • GitHub Check: test (macos-latest, 3.10)
  • GitHub Check: test (windows-latest, 3.14)
  • GitHub Check: test (windows-latest, 3.10)
  • GitHub Check: test (macos-latest, 3.14)
  • GitHub Check: test (ubuntu-latest, 3.13)
  • GitHub Check: test (ubuntu-latest, 3.10)
  • GitHub Check: Analyze (python)
🧰 Additional context used
📓 Path-based instructions (3)
**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

**/*.py: Keep Markdown vault files as the source of truth; store all derived indexes, caches, and vectors under paths.state_dir(), never in the vault.
Any operation writing multiple notes must journal pre-images through txn.Transaction while holding store.write_lock(); recovery must not overwrite notes edited after the crash.
Route all note writes through OmiStore; external writers should use notes.upsert_note. Preserve flocking, atomic rename, Lamport Rev: stamping, and soft-delete behavior. Deletes archive notes with Disabled: true; only omind mesh purge permanently removes them.
Use OmiStore.safe_name for every note read and write so path traversal remains impossible.
Keep store.py framework-free; it must not depend on FastAPI or MCP because both the CLI and web app build on it.
De-prioritize credential notes in search and gate suggestions using retrieve._CREDENTIAL_PENALTY, unless the query is about credentials; never steer agents into secrets notes.
MCP tools must not return unbounded output. Every list-shaped tool must paginate with limit, offset, total, and has_more via server._page.
Treat index.md and Memory Template.md as scaffolding rather than memories; reading them must not clear the consult gate, as represented by paths.NON_CONSULT_FILENAMES.
Index retrieval must preserve the fail-open fallback, including when disabled with OMI_INDEX_DISABLE=1; verify both indexed and fallback search paths.
Recency may only re-rank notes matched by content legs; it must never add unmatched notes to search results.
Do not strip code fences from [[wikilinks]] in the search index; lint.py intentionally remains the independent full-vault scanner.
link_targets() must preserve the author’s link casing for dangling-link reports; only link resolution should lowercase names.
Never mutate a NoteSummary returned from _cached_summary; use dataclasses.replace, as in store._indexed_search.
Coerce embedding results through `searc...

Files:

  • src/omind/backup.py
  • src/omind/hooks.py
  • tests/test_agents.py
  • src/omind/update.py
  • src/omind/store.py
  • src/omind/guard.py
  • tests/test_guard.py
  • tests/test_store.py
  • tests/test_provision.py
  • src/omind/provision.py
  • src/omind/mesh.py
  • src/omind/agents.py
**/*.{py,md}

📄 CodeRabbit inference engine (AGENTS.md)

Retrieval must fail open: every search layer returns None on errors and falls back to the older search path; test failure branches as well as successful searches.

Files:

  • src/omind/backup.py
  • src/omind/hooks.py
  • tests/test_agents.py
  • src/omind/update.py
  • docs/install-verification.md
  • src/omind/store.py
  • src/omind/guard.py
  • tests/test_guard.py
  • tests/test_store.py
  • tests/test_provision.py
  • src/omind/provision.py
  • src/omind/mesh.py
  • src/omind/agents.py
**/*.md

📄 CodeRabbit inference engine (AGENTS.md)

Documentation files must include the footer *Proudly Made in Nebraska. Go Big Red! 🌽 <https://xkcd.com/2347/>*; the README uses the centered banner variant.

Files:

  • docs/install-verification.md
🪛 markdownlint-cli2 (0.23.2)
docs/install-verification.md

[warning] 20-20: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


[warning] 22-22: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


[warning] 164-164: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🔇 Additional comments (4)
src/omind/guard.py (2)

470-476: LGTM!


1562-1563: LGTM!

src/omind/hooks.py (1)

377-396: LGTM!

Also applies to: 575-576

tests/test_agents.py (1)

28-36: LGTM!

Comment on lines +19 to +23
2. **Write tests go to a throwaway vault**, never the real OMI folder:
```sh
export TESTVAULT="$(mktemp -d)/vault"; mkdir -p "$TESTVAULT/OMI"
```
Pass `--vault "$TESTVAULT" --folder OMI` on every write/index/mesh row.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Fix the reported Markdown fence violations.

markdownlint-cli2 reports MD031 because the shell fence at Lines 20-22 lacks surrounding blank lines. It reports MD040 because the reporting example at Line 164 has no language. Add blank lines around the shell fence and mark the reporting fence as text.

Also applies to: 164-172

🧰 Tools
🪛 markdownlint-cli2 (0.23.2)

[warning] 20-20: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


[warning] 22-22: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/install-verification.md` around lines 19 - 23, Add blank lines
immediately before and after the shell code fence in the installation
verification instructions, and label the reporting example fence around the
reporting section with the text language. Preserve the existing command and
reporting content unchanged.

Source: Linters/SAST tools

Comment on lines +43 to +59
## B. Wiring — what `omind setup` is supposed to have installed

| ID | Command | Pass criterion |
|----|---------|----------------|
| B1 | `omind doctor` | Exit 0, **zero problems**. Warnings triaged individually against this table |
| B2 | `omind doctor` line "MCP server 'omi'" | `[✓]`, not "differs from the expected `omind node` command". This is the single most common drift after a manual edit of `~/.claude.json` |
| B3 | `jq '.mcpServers.omi' ~/.claude.json` | Command array is `[<omind>, node, --vault, <vault>, --folder, OMI]` — an absolute interpreter path pointing into a venv that no longer exists is the classic post-reinstall break |
| B4 | `jq '.hooks \| keys' ~/.claude/settings.json` | Contains `PreToolUse`, `PostToolUse`, `Stop`, `SessionStart`, `UserPromptSubmit` |
| B5 | `jq -r '.hooks[][]\|.hooks[]?.command' ~/.claude/settings.json` | Contains `omind hook PostToolUse`, `omind hook Stop`, `omind hook SessionStart`, plus the four managed scripts: `omi-guard.sh`, `omi-gate-reset.sh`, `omi-enforce.py`, `secret-output-guard.sh` (and `git-fresh-base.sh` where repo rules apply) |
| B6 | `ls -l ~/.claude/hooks/` | All hook scripts present and executable (`0755`). Managed guard scripts are expected to be root-owned/write-protected — a user-writable `omi-guard.sh` defeats self-protection |
| B7 | `cat ~/.claude/hooks/.omind-provision.json` | Manifest present; its recorded SHAs match the shipped hook resources. Mismatch = **hookset drift** (someone hand-edited a managed script); repair with `omind setup` |
| B7a | `omind doctor` line "auto-memory hooks run a non-canonical omind" | Absent. Present = hooks are pinned to a different install than `~/.local/bin/omind`, so `self-update` will never reach them — the failure that let one box run 8.1.1 hooks under an 8.2.0 binary indefinitely |
| B7b | Confirm `omind setup` can actually write `~/.claude/settings.json` | Writable by the user. If it is root-owned **and** immutable (`chattr +i`), setup dies with `PermissionError` and every "run `omind setup`" repair instruction in this matrix is impossible until an operator clears it — verify before trusting any B/C row's remediation |
| B8 | `test -f ~/.claude/skills/omind/SKILL.md` | Present — this is how agents discover authoritative command syntax |
| B9 | `omind doctor` line "seed files present" | `[✓]` — `index.md` + note template exist in the OMI folder |
| B10 | `test -f "$OMI/.obsidian/app.json"` | Obsidian config seeded |
| B11 | `git -C "$OMI" config --get-regexp 'merge\.(omi\|ours)'` + `cat "$OMI/.gitattributes"` | Both merge drivers configured and `.gitattributes` routes `*.md` to the `omi` driver — without this, mesh sync silently produces conflict markers in notes |

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Verify the canonical executable for every supported harness.

The B3 and B5 criteria accept <omind> and omind hook ... without checking the exact ~/.local/bin/omind path. They inspect only Claude configuration, while D1 exercises Hermes, OpenCode, Codex, Gemini, and OpenClaw without checking their installed wiring. A stale virtual-environment binary or non-Claude drift can pass this matrix and remain unreachable by self-update. Compare each MCP and hook command with the canonical executable, and add equivalent checks for every supported harness. src/omind/agents.py:1602-1655 already compares registration with desired_server_entry().

Also applies to: 80-84

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/install-verification.md` around lines 43 - 59, Update the verification
matrix so B3 and B5 require MCP and hook commands to resolve to the canonical
~/.local/bin/omind executable, not merely the command name. Add equivalent
wiring checks for every supported harness exercised by D1: Hermes, OpenCode,
Codex, Gemini, and OpenClaw. Reuse the existing desired_server_entry()
comparison behavior from agents.py to detect stale virtual-environment binaries
and non-Claude drift.

Comment on lines +124 to +134
## G. Retrieval

| ID | Command | Pass criterion |
|----|---------|----------------|
| G1 | `omind doctor` line "search index: FTS5 available" | `[✓]`. FTS5 missing = the keyword path is degraded to a scan |
| G2 | `omind doctor` line "semantic search" | `[✓]` if `omind[embed]` was intended. "off (keyword path) — model2vec not importable" is a legitimate SKIP only if the install deliberately omitted the extra; it costs ~20pp recall@1 |
| G3 | `omind doctor` line "search index: … stale note(s)" | Zero stale notes and index age consistent with the write timer. Non-zero stale = run `omind reindex --rebuild` and re-check |
| G4 | `omind search '<known phrase>'` **RO** | Returns the expected note in the top hits |
| G5 | `omind reindex --vault "$TESTVAULT" --folder OMI` | Exits 0; index rebuilt under the write lock |
| G6 | `omind bench --vault "$TESTVAULT" --folder OMI` | Reports index-build, search latency, capsule build, recall token cost. Record as the **install's baseline** — this row's value is the number, compared over time |
| G7 | `omind graph stats` / `graph orphans` / `graph dangling` **RO** | All exit 0 and return structured output |

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Add a fallback-search acceptance row.

The G section tests indexed success, stale notes, and reindex success, but it does not run OMI_INDEX_DISABLE=1 or force an index error. A broken index can pass this matrix even though fail-open behavior is untested. Add a known-phrase search with the index disabled and verify that the fallback returns the expected note. Also cover an injected index failure if the test harness supports it.

As per coding guidelines: Retrieval must fail open: every search layer returns None on errors and falls back to the older search path; test failure branches as well as successful searches.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/install-verification.md` around lines 124 - 134, Add a retrieval
acceptance row in section G that runs a known-phrase search with
OMI_INDEX_DISABLE=1 and verifies the expected note is returned through the
fallback path. If the test harness supports index-error injection, add coverage
for that failure case as well, confirming search still falls back successfully
and preserves the documented fail-open behavior.

Source: Coding guidelines

| I1 | `omind checkpoint --vault "$TESTVAULT" --folder OMI` | Produces/updates a daily worklog note |
| I2 | `systemctl --user list-timers \| grep -i omind` (or the platform equivalent) | The checkpoint timer is installed and scheduled if `install-timer` was intended |
| I3 | `omind rollup --vault "$TESTVAULT" --folder OMI` | Compacts dailies; default archives rather than deletes |
| I4 | `omind consolidate --vault "$TESTVAULT" --folder OMI` | Proposes merges **without mutating** the vault (verify with `git -C status`/checksums before and after) |

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Correct the git -C verification command.

git -C status treats status as the repository path. It does not run git status. The I4 check therefore cannot verify that consolidation did not mutate the vault. Use git -C "$TESTVAULT/OMI" status --short before and after, or provide a valid checksum comparison.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/install-verification.md` at line 156, Correct the I4 verification step
in the documentation by replacing the invalid git -C status reference with a
valid command such as git -C "$TESTVAULT/OMI" status --short, and ensure the
before-and-after checks compare the vault state without mutation.


```
omind install verification — <host> — <version> — <date>
A: 5/5 B: 11/11 C: 6/7 (C7 FAIL) D: 11/13 (D0,D1 FAIL) E: 6/6

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Fix the B-group denominator.

The table defines 13 B rows: B1-B11, B7a, and B7b. The example reports B: 11/11, so it omits two checks. Change it to B: 13/13, or explicitly exclude B7a and B7b from group totals.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/install-verification.md` at line 166, Update the verification summary’s
B-group total to account for all 13 defined B checks, changing the example from
B: 11/11 to B: 13/13; do not exclude B7a or B7b.

Comment thread src/omind/provision.py Outdated
Comment on lines +316 to +323
try:
tokens = shlex.split(command_text)
except ValueError: # unbalanced quotes in a hand-edited settings.json
return None
for token, following in zip(tokens, tokens[1:], strict=False):
if following == "hook" and os.sep in token:
return token
return None

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Make hook executable detection platform-aware and require an absolute path.

Line 321 treats any path containing os.sep as absolute. A relative command such as ./venv/bin/omind hook Stop is reported as a stale absolute pin.

shlex.split() also consumes Windows backslashes. A command such as C:\Users\u\Scripts\omind.EXE hook Stop is not detected. doctor can then report healthy hooks after an update leaves a Windows hook pinned to an old executable.

Use platform-correct command parsing and an absolute-path check. Add regression cases for relative paths and Windows paths.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/omind/provision.py` around lines 316 - 323, Update the hook executable
detection around shlex.split and the token scan to use platform-correct parsing,
preserve Windows backslashes, and only return the executable when its path is
absolute rather than merely containing os.sep. Extend the regression coverage
for relative commands such as ./venv/bin/omind and Windows absolute paths such
as C:\Users\u\Scripts\omind.EXE.

Comment thread src/omind/store.py
Comment on lines +951 to +956
try:
return self._validated_name(name)
except NoteError:
fallback = self._name_from_title(name)
if fallback is not None:
return fallback

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Reject traversal-shaped names before title fallback.

Lines 951-956 sanitize every rejected name. If outside.md exists in the vault, safe_name("../outside") resolves it. If etc passwd.md exists, safe_name("/etc/passwd") resolves it. The path does not escape the vault, but invalid traversal-shaped input becomes an alias that write paths can update, disable, or purge.

  • src/omind/store.py#L951-L956: Reject absolute paths and names with a .. path segment before calling _name_from_title.
  • tests/test_store.py#L902-L909: Create matching sanitized files inside tmp_path and assert that ../outside and /etc/passwd still raise NoteError.
Proposed fix
 def _name_from_title(self, name: str) -> Path | None:
     """The existing note whose filename this *title* sanitizes to, if any."""
+    raw = (name or "").strip()
+    if raw.startswith(("/", "\\")) or ".." in re.split(r"[\\/]+", raw):
+        return None
     try:
         candidate = self._validated_name(self.filename_for_title(name))

As per coding guidelines, OmiStore.safe_name must keep path traversal impossible.

📍 Affects 2 files
  • src/omind/store.py#L951-L956 (this comment)
  • tests/test_store.py#L902-L909
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/omind/store.py` around lines 951 - 956, Update OmiStore.safe_name around
_validated_name and _name_from_title to reject absolute paths and any input
containing a “..” path segment before title fallback; preserve NoteError for
these traversal-shaped names. In tests/test_store.py lines 902-909, create
matching sanitized files under tmp_path and assert that ../outside and
/etc/passwd still raise NoteError.

Source: Coding guidelines

Comment thread src/omind/update.py
Comment on lines +226 to +235
from omind.provision import Provisioner, SetupConfig, default_vault_path

vault = default_vault_path()
# Hook scripts, the MCP entry, and the skill are all rewritten by the new
# binary — otherwise a release that changes any of them lands only on boxes
# where someone remembered to re-run `omind setup` by hand.
try:
actions = Provisioner(config=SetupConfig(vault=vault), log=lambda _m: None).run()
if actions:
log(f"re-provisioned wiring ({len(actions)} change(s)).")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Use the installed setup configuration for post-update healing.

This always creates SetupConfig(vault=default_vault_path()). It therefore resets folder to OMI and agent to claude.

If the user configured another vault, folder, or agent harness, a successful update can create or modify the default Claude installation instead of healing the active installation. Resolve the persisted active setup configuration before provisioning. If no configuration is available, skip provisioning and report a warning.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/omind/update.py` around lines 226 - 235, Update the post-update
provisioning flow around Provisioner.run to load the persisted active
SetupConfig instead of constructing one with default_vault_path(). Use the
configured vault, folder, and agent values when re-provisioning; if no saved
setup configuration exists, skip provisioning and emit a warning rather than
modifying the defaults.

Comment thread tests/test_guard.py
Comment on lines +1383 to +1398
def test_guard_pause_is_capped(capsys: pytest.CaptureFixture[str]) -> None:
"""A week-long pause is a disable with extra steps: it silently masks the
enforcement check for the duration. One box was found paused for 185h."""
assert guard.run_guard("pause", duration="185h") == 0
out = capsys.readouterr().out
assert "cap" in out
remaining = guard.pause_remaining()
assert 0 < remaining <= guard._MAX_PAUSE_SECONDS
guard.resume_gate()


def test_guard_pause_under_the_cap_is_untouched(capsys: pytest.CaptureFixture[str]) -> None:
assert guard.run_guard("pause", duration="30m") == 0
assert 0 < guard.pause_remaining() <= 1800
assert "cap" not in capsys.readouterr().out
guard.resume_gate()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Guarantee pause cleanup after assertion failures.

guard.resume_gate() runs only after the assertions at Line [1388] and Line [1397]. If an assertion fails, the pause sentinel remains active and can affect later tests. Put each cleanup call in a finally block, or use an autouse fixture that re-arms the gate.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_guard.py` around lines 1383 - 1398, Ensure both pause tests always
clean up the pause sentinel by moving guard.resume_gate() into a finally block
surrounding each test’s assertions and output checks. Update
test_guard_pause_is_capped and test_guard_pause_under_the_cap_is_untouched
without changing their existing validation behavior.

Comment thread tests/test_guard.py
Comment on lines +1386 to +1397
assert guard.run_guard("pause", duration="185h") == 0
out = capsys.readouterr().out
assert "cap" in out
remaining = guard.pause_remaining()
assert 0 < remaining <= guard._MAX_PAUSE_SECONDS
guard.resume_gate()


def test_guard_pause_under_the_cap_is_untouched(capsys: pytest.CaptureFixture[str]) -> None:
assert guard.run_guard("pause", duration="30m") == 0
assert 0 < guard.pause_remaining() <= 1800
assert "cap" not in capsys.readouterr().out

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Assert the effective duration, not only the upper bound.

The capped test passes for any positive duration at Line [1389]. The under-cap test passes for any positive duration up to 1,800 seconds at Line [1396]. Compare the remaining duration with _MAX_PAUSE_SECONDS and 1,800 using a small tolerance, or inject a deterministic clock. Otherwise, an implementation that under-pauses every request can pass both tests.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_guard.py` around lines 1386 - 1397, Strengthen test_guard_pause
duration assertions by verifying the effective remaining time is approximately
_MAX_PAUSE_SECONDS for the capped 185h request and approximately 1,800 seconds
for the 30m request, using a small timing tolerance or deterministic clock
injection. Keep the existing cap-output and resume behavior checks intact.

`_hook_exe_path` tested `os.sep in token`, so a POSIX-style pin read on
Windows (or the reverse) was missed -- precisely the stale-install case the
check exists to catch. settings.json is portable data; both separators count.

Matched with a regex rather than shlex for the same reason: POSIX-mode shlex
treats the backslashes in a Windows path as escapes and silently flattens the
path away, which is how the Windows CI job caught this.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T3gMTVBvDX84KKe2h8HXvJ
@CryptoJones
CryptoJones merged commit 8f21e44 into main Aug 11, 2026
16 checks passed
@CryptoJones
CryptoJones deleted the fix/canonical-hook-paths-and-guard-precision branch August 11, 2026 11:08
@CryptoJones CryptoJones mentioned this pull request Aug 11, 2026
CryptoJones added a commit that referenced this pull request Aug 11, 2026
Patch release rolling up #229: hooks pinned to a canonical omind so an update
actually reaches them, recall-note accepting titles that contain '/', a
re-provision + index warm on self-update, a 4h cap on guard pause, and a
deny-rate health metric in doctor. Adds docs/install-verification.md.

Version bumped in lockstep: pyproject.toml, src/omind/__init__.py, uv.lock.


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

Co-authored-by: Claude Opus 5 <noreply@anthropic.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