Skip to content
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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [3.7.4] - 2026-07-01

### Fixed
- Allow read-only shell inspection of global agent config and hook paths while
still requiring explicit current-turn authorization for shell mutations.

## [3.7.3] - 2026-07-01

### Fixed
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "omind"
version = "3.7.3"
version = "3.7.4"
description = "Reproduce the OMI/Obsidian memory integration for AI agents, plus a local web app to view, edit, and add memory entries."
readme = "README.md"
requires-python = ">=3.10"
Expand Down
2 changes: 1 addition & 1 deletion src/omind/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
# Copyright 2026 Aaron K. Clark
"""omind — OMI/Obsidian memory tooling for AI agents."""

__version__ = "3.7.3"
__version__ = "3.7.4"
15 changes: 14 additions & 1 deletion src/omind/guard.py
Original file line number Diff line number Diff line change
Expand Up @@ -462,6 +462,15 @@ def clear_all_gates() -> None:
r")\b",
re.IGNORECASE,
)
_GLOBAL_MUTATING_BASH_RE = re.compile(
r"(?:^|[;&|\n(]\s*)(?:"
r"chmod|chown|cp|dd|ed|ex|install|mv|rm|tee|touch|truncate|"
r"sed\b[^;&|\n]*\s-i\b|perl\b[^;&|\n]*\s-i\b|"
r"python3?\b[^;&|\n]*(?:write_text|write_bytes|open\([^;&|\n]*[\"']a|"
r"open\([^;&|\n]*[\"']w)|"
r"node\b[^;&|\n]*(?:writeFile|appendFile)"
r")\b|>|>>"
)


def _opt_in_satisfied(opt_in: str, command: str) -> bool:
Expand Down Expand Up @@ -551,7 +560,11 @@ def _is_global_config_mutation(action: dict[str, Any]) -> bool:
haystack = " ".join(
part for part in (str(action.get("command") or ""), _action_path(action)) if part
).replace("\\", "/")
return bool(_GLOBAL_CONFIG_RE.search(haystack))
if not _GLOBAL_CONFIG_RE.search(haystack):
return False
if tool in _WRITE_TOOLS:
return True
return bool(_GLOBAL_MUTATING_BASH_RE.search(str(action.get("command") or "")))


def _turn_has_explicit_global_auth(session: str) -> bool:
Expand Down
26 changes: 26 additions & 0 deletions tests/test_guard.py
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,32 @@ def test_global_config_mutation_requires_explicit_turn_authorization() -> None:
guard.clear_gate("global")


def test_global_config_read_only_shell_commands_are_not_mutations() -> None:
hook_path = Path.home() / ".claude" / "hooks" / "omi-guard.sh"

guard.begin_turn("global-read", "Can you inspect the hook?")
guard.mark_consulted("global-read")
allowed = guard.decide(
{
"tool": "Bash",
"command": f"stat {hook_path}",
"session": "global-read",
}
)
assert allowed.allow

blocked = guard.decide(
{
"tool": "Bash",
"command": f"chmod +x {hook_path}",
"session": "global-read",
}
)
assert not blocked.allow
assert blocked.rule_id == "global-config-explicit-auth"
guard.clear_gate("global-read")


def test_clear_gate_reaps_legacy_tmp_sentinels(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
Expand Down
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.