history-graph-protocol 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- history_graph_protocol-0.2.0/.claude/hooks/post_bash_hgp.py +78 -0
- history_graph_protocol-0.2.0/.claude/hooks/pre_bash_hgp.py +138 -0
- history_graph_protocol-0.2.0/.claude/hooks/pre_tool_use_hgp.py +46 -0
- history_graph_protocol-0.2.0/.claude/settings.json +35 -0
- history_graph_protocol-0.2.0/.claude/settings.local.json +14 -0
- history_graph_protocol-0.2.0/.gemini/hooks/post_bash_hgp.py +82 -0
- history_graph_protocol-0.2.0/.gemini/hooks/pre_bash_hgp.py +138 -0
- history_graph_protocol-0.2.0/.gemini/hooks/pre_tool_use_hgp.py +48 -0
- history_graph_protocol-0.2.0/.gemini/settings.json +38 -0
- history_graph_protocol-0.2.0/.gitignore +13 -0
- history_graph_protocol-0.2.0/.python-version +1 -0
- history_graph_protocol-0.2.0/CLAUDE.md +78 -0
- history_graph_protocol-0.2.0/HGP_Master_Plan.md +66 -0
- history_graph_protocol-0.2.0/HGP_Technical_Design.md +993 -0
- history_graph_protocol-0.2.0/LICENSE +21 -0
- history_graph_protocol-0.2.0/PKG-INFO +228 -0
- history_graph_protocol-0.2.0/README.md +201 -0
- history_graph_protocol-0.2.0/conversation_export.md +1316 -0
- history_graph_protocol-0.2.0/docs/CONTRIBUTING.md +178 -0
- history_graph_protocol-0.2.0/docs/architecture.md +547 -0
- history_graph_protocol-0.2.0/docs/plans/2026-03-02-hgp-v1-implementation.md +2018 -0
- history_graph_protocol-0.2.0/docs/plans/2026-03-06-v2-memory-tier.md +841 -0
- history_graph_protocol-0.2.0/docs/plans/2026-03-19-v3-evidence-trail.md +245 -0
- history_graph_protocol-0.2.0/docs/plans/2026-03-22-v3-evidence-trail.md +245 -0
- history_graph_protocol-0.2.0/docs/plans/2026-03-25-documentation.md +512 -0
- history_graph_protocol-0.2.0/docs/plans/2026-04-01-v4-file-tracking.md +323 -0
- history_graph_protocol-0.2.0/docs/plans/2026-04-02-120316-v4-file-tracking-followup-remediation.md +220 -0
- history_graph_protocol-0.2.0/docs/plans/2026-04-02-122419-v4-file-tracking-second-followup-remediation.md +162 -0
- history_graph_protocol-0.2.0/docs/plans/2026-04-02-125505-v4-file-tracking-third-followup-remediation.md +177 -0
- history_graph_protocol-0.2.0/docs/plans/2026-04-02-170041-v4-file-tracking-fourth-followup-remediation.md +178 -0
- history_graph_protocol-0.2.0/docs/plans/2026-04-02-171348-v4-file-tracking-fifth-followup-remediation.md +118 -0
- history_graph_protocol-0.2.0/docs/plans/2026-04-02-171348-v5-reconciler-and-bash-hooks-remediation.md +166 -0
- history_graph_protocol-0.2.0/docs/plans/2026-04-02-v4-file-tracking-remediation.md +270 -0
- history_graph_protocol-0.2.0/docs/plans/2026-04-03-144423-v5-reconciler-and-bash-hooks-remediation-plan.md +170 -0
- history_graph_protocol-0.2.0/docs/plans/2026-04-03-145145-v5-three-commit-remediation-plan.md +159 -0
- history_graph_protocol-0.2.0/docs/plans/2026-04-03-151825-8aaaa60-followup-plan.md +135 -0
- history_graph_protocol-0.2.0/docs/plans/2026-04-03-154847-ebb5960-followup-plan.md +88 -0
- history_graph_protocol-0.2.0/docs/plans/2026-04-03-161740-overall-codebase-remediation-plan.md +189 -0
- history_graph_protocol-0.2.0/docs/plans/2026-04-03-163319-overall-codebase-remediation-plan-subagents.md +206 -0
- history_graph_protocol-0.2.0/docs/plans/2026-04-03-165841-00bc020-followup-plan.md +70 -0
- history_graph_protocol-0.2.0/docs/plans/2026-04-03-170820-9d9fe96-followup-plan.md +46 -0
- history_graph_protocol-0.2.0/docs/plans/2026-04-03-full-audit-remediation-plan.md +692 -0
- history_graph_protocol-0.2.0/docs/plans/2026-04-03-v5-reconciler-and-bash-hooks.md +322 -0
- history_graph_protocol-0.2.0/docs/plans/2026-04-04-001348-0ab4763-followup-plan.md +111 -0
- history_graph_protocol-0.2.0/docs/plans/2026-04-04-002912-4e206ba-followup-plan.md +66 -0
- history_graph_protocol-0.2.0/docs/plans/2026-04-04-003533-1a7cfac-followup-plan.md +38 -0
- history_graph_protocol-0.2.0/docs/plans/2026-04-04-005926-d00a855-followup-plan.md +197 -0
- history_graph_protocol-0.2.0/docs/plans/2026-04-04-011012-0ab4763-d00a855-followup-plan.md +180 -0
- history_graph_protocol-0.2.0/docs/plans/2026-04-04-011953-d276ec9-followup-plan.md +95 -0
- history_graph_protocol-0.2.0/docs/plans/2026-04-05-230725-5f86cd1-followup-plan.md +38 -0
- history_graph_protocol-0.2.0/docs/plans/2026-04-05-232532-59f4967-followup-plan.md +65 -0
- history_graph_protocol-0.2.0/docs/plans/2026-04-05-233553-67da2f4-followup-plan.md +48 -0
- history_graph_protocol-0.2.0/docs/plans/2026-04-05-234435-634bed7-followup-plan.md +52 -0
- history_graph_protocol-0.2.0/docs/plans/2026-04-05-235109-33091d1-followup-plan.md +39 -0
- history_graph_protocol-0.2.0/docs/plans/2026-04-07-010026-522f901-followup-plan.md +80 -0
- history_graph_protocol-0.2.0/docs/plans/2026-04-07-011841-2031368-followup-plan.md +63 -0
- history_graph_protocol-0.2.0/docs/plans/2026-04-07-112432-4cc9755-followup-plan.md +41 -0
- history_graph_protocol-0.2.0/docs/plans/2026-04-07-120244-264c5ae-followup-plan.md +48 -0
- history_graph_protocol-0.2.0/docs/plans/2026-04-07-130954-e470064-followup-plan.md +40 -0
- history_graph_protocol-0.2.0/docs/plans/2026-04-07-140903-43d5ec6-followup-plan.md +69 -0
- history_graph_protocol-0.2.0/docs/plans/2026-04-07-153656-0a96afc-followup-plan.md +63 -0
- history_graph_protocol-0.2.0/docs/plans/2026-04-07-155219-b0e75ce-followup-plan.md +51 -0
- history_graph_protocol-0.2.0/docs/plans/2026-04-07-hgp-phase2-worktree-isolation-options.md +202 -0
- history_graph_protocol-0.2.0/docs/plans/2026-04-07-hgp-storage-topology-recommendation.md +362 -0
- history_graph_protocol-0.2.0/docs/plans/2026-04-07-task42-repo-local-storage-plan.md +253 -0
- history_graph_protocol-0.2.0/docs/reviews/2026-03-19-v2-main-merge-review.txt +72 -0
- history_graph_protocol-0.2.0/docs/reviews/2026-04-02-120316-v4-file-tracking-followup-review.md +147 -0
- history_graph_protocol-0.2.0/docs/reviews/2026-04-02-122419-v4-file-tracking-second-followup-review.md +133 -0
- history_graph_protocol-0.2.0/docs/reviews/2026-04-02-125505-v4-file-tracking-third-followup-review.md +146 -0
- history_graph_protocol-0.2.0/docs/reviews/2026-04-02-170041-v4-file-tracking-fourth-followup-review.md +147 -0
- history_graph_protocol-0.2.0/docs/reviews/2026-04-02-171348-v4-file-tracking-fifth-followup-review.md +86 -0
- history_graph_protocol-0.2.0/docs/reviews/2026-04-02-171348-v5-reconciler-and-bash-hooks-review.md +153 -0
- history_graph_protocol-0.2.0/docs/reviews/2026-04-02-v4-file-tracking-review.md +108 -0
- history_graph_protocol-0.2.0/docs/reviews/2026-04-03-144423-v5-reconciler-and-bash-hooks-remediation-review.md +163 -0
- history_graph_protocol-0.2.0/docs/reviews/2026-04-03-145145-v5-three-commit-review.md +151 -0
- history_graph_protocol-0.2.0/docs/reviews/2026-04-03-151825-8aaaa60-review.md +130 -0
- history_graph_protocol-0.2.0/docs/reviews/2026-04-03-154847-ebb5960-review.md +100 -0
- history_graph_protocol-0.2.0/docs/reviews/2026-04-03-161740-overall-codebase-review.md +166 -0
- history_graph_protocol-0.2.0/docs/reviews/2026-04-03-163319-overall-codebase-review-subagents.md +201 -0
- history_graph_protocol-0.2.0/docs/reviews/2026-04-03-165841-00bc020-review.md +122 -0
- history_graph_protocol-0.2.0/docs/reviews/2026-04-03-170820-9d9fe96-review.md +65 -0
- history_graph_protocol-0.2.0/docs/reviews/2026-04-03-audit-architecture.md +228 -0
- history_graph_protocol-0.2.0/docs/reviews/2026-04-03-audit-code-quality.md +289 -0
- history_graph_protocol-0.2.0/docs/reviews/2026-04-03-audit-security.md +200 -0
- history_graph_protocol-0.2.0/docs/reviews/2026-04-03-audit-silent-failures.md +467 -0
- history_graph_protocol-0.2.0/docs/reviews/2026-04-03-audit-test-coverage.md +485 -0
- history_graph_protocol-0.2.0/docs/reviews/2026-04-04-001348-0ab4763-review.md +143 -0
- history_graph_protocol-0.2.0/docs/reviews/2026-04-04-002912-4e206ba-review.md +97 -0
- history_graph_protocol-0.2.0/docs/reviews/2026-04-04-003533-1a7cfac-review.md +69 -0
- history_graph_protocol-0.2.0/docs/reviews/2026-04-04-005926-d00a855-review.md +222 -0
- history_graph_protocol-0.2.0/docs/reviews/2026-04-04-011012-0ab4763-d00a855-review.md +156 -0
- history_graph_protocol-0.2.0/docs/reviews/2026-04-04-011953-d276ec9-review.md +129 -0
- history_graph_protocol-0.2.0/docs/reviews/2026-04-05-230725-5f86cd1-review.md +68 -0
- history_graph_protocol-0.2.0/docs/reviews/2026-04-05-232532-59f4967-review.md +75 -0
- history_graph_protocol-0.2.0/docs/reviews/2026-04-05-233553-67da2f4-review.md +63 -0
- history_graph_protocol-0.2.0/docs/reviews/2026-04-05-234435-634bed7-review.md +93 -0
- history_graph_protocol-0.2.0/docs/reviews/2026-04-05-235109-33091d1-review.md +64 -0
- history_graph_protocol-0.2.0/docs/reviews/2026-04-07-010026-522f901-review.md +102 -0
- history_graph_protocol-0.2.0/docs/reviews/2026-04-07-011841-2031368-review.md +91 -0
- history_graph_protocol-0.2.0/docs/reviews/2026-04-07-112432-4cc9755-review.md +67 -0
- history_graph_protocol-0.2.0/docs/reviews/2026-04-07-120244-264c5ae-review.md +59 -0
- history_graph_protocol-0.2.0/docs/reviews/2026-04-07-130954-e470064-review.md +61 -0
- history_graph_protocol-0.2.0/docs/reviews/2026-04-07-140903-43d5ec6-review.md +63 -0
- history_graph_protocol-0.2.0/docs/reviews/2026-04-07-153656-0a96afc-review.md +112 -0
- history_graph_protocol-0.2.0/docs/reviews/2026-04-07-155219-b0e75ce-review.md +80 -0
- history_graph_protocol-0.2.0/docs/tools-reference.md +1074 -0
- history_graph_protocol-0.2.0/docs/usage-patterns.md +612 -0
- history_graph_protocol-0.2.0/pyproject.toml +59 -0
- history_graph_protocol-0.2.0/src/hgp/__init__.py +3 -0
- history_graph_protocol-0.2.0/src/hgp/cas.py +103 -0
- history_graph_protocol-0.2.0/src/hgp/dag.py +153 -0
- history_graph_protocol-0.2.0/src/hgp/db.py +618 -0
- history_graph_protocol-0.2.0/src/hgp/errors.py +43 -0
- history_graph_protocol-0.2.0/src/hgp/hooks/__init__.py +0 -0
- history_graph_protocol-0.2.0/src/hgp/hooks/claude/__init__.py +0 -0
- history_graph_protocol-0.2.0/src/hgp/hooks/claude/post_bash_hgp.py +78 -0
- history_graph_protocol-0.2.0/src/hgp/hooks/claude/pre_bash_hgp.py +138 -0
- history_graph_protocol-0.2.0/src/hgp/hooks/claude/pre_tool_use_hgp.py +46 -0
- history_graph_protocol-0.2.0/src/hgp/hooks/gemini/__init__.py +0 -0
- history_graph_protocol-0.2.0/src/hgp/hooks/gemini/post_bash_hgp.py +82 -0
- history_graph_protocol-0.2.0/src/hgp/hooks/gemini/pre_bash_hgp.py +138 -0
- history_graph_protocol-0.2.0/src/hgp/hooks/gemini/pre_tool_use_hgp.py +48 -0
- history_graph_protocol-0.2.0/src/hgp/lease.py +133 -0
- history_graph_protocol-0.2.0/src/hgp/models.py +153 -0
- history_graph_protocol-0.2.0/src/hgp/project.py +71 -0
- history_graph_protocol-0.2.0/src/hgp/reconciler.py +174 -0
- history_graph_protocol-0.2.0/src/hgp/server.py +1085 -0
- history_graph_protocol-0.2.0/tests/__init__.py +0 -0
- history_graph_protocol-0.2.0/tests/conftest.py +29 -0
- history_graph_protocol-0.2.0/tests/test_bash_hooks.py +279 -0
- history_graph_protocol-0.2.0/tests/test_cas.py +131 -0
- history_graph_protocol-0.2.0/tests/test_dag.py +208 -0
- history_graph_protocol-0.2.0/tests/test_db.py +933 -0
- history_graph_protocol-0.2.0/tests/test_file_ops.py +841 -0
- history_graph_protocol-0.2.0/tests/test_install_hooks.py +90 -0
- history_graph_protocol-0.2.0/tests/test_integration.py +167 -0
- history_graph_protocol-0.2.0/tests/test_lease.py +67 -0
- history_graph_protocol-0.2.0/tests/test_models.py +129 -0
- history_graph_protocol-0.2.0/tests/test_project.py +39 -0
- history_graph_protocol-0.2.0/tests/test_reconciler.py +541 -0
- history_graph_protocol-0.2.0/tests/test_server_tools.py +1317 -0
- history_graph_protocol-0.2.0/uv.lock +806 -0
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""PostToolUse hook for Bash: detect and report actual file changes after Bash commands.
|
|
2
|
+
|
|
3
|
+
Only runs git status when the Pre-Bash hook wrote a marker file indicating a
|
|
4
|
+
potentially mutating command was about to execute. This avoids the overhead of
|
|
5
|
+
git status on every read-only Bash call.
|
|
6
|
+
|
|
7
|
+
Known limitation: .gitignore'd files won't appear in the report.
|
|
8
|
+
|
|
9
|
+
Marker file: /tmp/.hgp_bash_mutating_<ppid>
|
|
10
|
+
"""
|
|
11
|
+
import json
|
|
12
|
+
import os
|
|
13
|
+
import subprocess
|
|
14
|
+
import sys
|
|
15
|
+
|
|
16
|
+
_TIMEOUT_SECS = 2
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _marker_path() -> str:
|
|
20
|
+
return f"/tmp/.hgp_bash_mutating_{os.getppid()}"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _consume_marker() -> bool:
|
|
24
|
+
"""Return True and remove marker if it exists, False otherwise."""
|
|
25
|
+
path = _marker_path()
|
|
26
|
+
try:
|
|
27
|
+
os.unlink(path)
|
|
28
|
+
return True
|
|
29
|
+
except FileNotFoundError:
|
|
30
|
+
return False
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _git_changed_files(cwd: str) -> list[str]:
|
|
34
|
+
"""Run git status --porcelain and return list of changed file entries."""
|
|
35
|
+
try:
|
|
36
|
+
result = subprocess.run(
|
|
37
|
+
["git", "status", "--porcelain"],
|
|
38
|
+
cwd=cwd,
|
|
39
|
+
capture_output=True,
|
|
40
|
+
text=True,
|
|
41
|
+
timeout=_TIMEOUT_SECS,
|
|
42
|
+
)
|
|
43
|
+
if result.returncode != 0:
|
|
44
|
+
return []
|
|
45
|
+
lines = [l for l in result.stdout.splitlines() if l.strip()]
|
|
46
|
+
return lines
|
|
47
|
+
except (subprocess.TimeoutExpired, FileNotFoundError, OSError):
|
|
48
|
+
return []
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def main() -> None:
|
|
52
|
+
try:
|
|
53
|
+
event = json.loads(sys.stdin.read())
|
|
54
|
+
except (json.JSONDecodeError, ValueError):
|
|
55
|
+
sys.exit(0)
|
|
56
|
+
|
|
57
|
+
if event.get("tool_name") != "Bash":
|
|
58
|
+
sys.exit(0)
|
|
59
|
+
|
|
60
|
+
if not _consume_marker():
|
|
61
|
+
# No marker → Pre hook didn't flag this as mutating; skip git status
|
|
62
|
+
sys.exit(0)
|
|
63
|
+
|
|
64
|
+
cwd = os.getcwd()
|
|
65
|
+
changed = _git_changed_files(cwd)
|
|
66
|
+
if not changed:
|
|
67
|
+
sys.exit(0)
|
|
68
|
+
|
|
69
|
+
lines_str = "\n ".join(changed)
|
|
70
|
+
print(
|
|
71
|
+
f"[HGP] Bash command changed tracked files (use hgp_* tools for history):\n {lines_str}",
|
|
72
|
+
file=sys.stderr,
|
|
73
|
+
)
|
|
74
|
+
sys.exit(0)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
if __name__ == "__main__":
|
|
78
|
+
main()
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"""PreToolUse hook for Bash: warn when shell commands may mutate files outside HGP.
|
|
2
|
+
|
|
3
|
+
This hook is non-blocking (always exit 0). It prints a warning to stderr when
|
|
4
|
+
it detects shell patterns that typically write or delete files, reminding the
|
|
5
|
+
agent to use hgp_* tools for tracked file operations.
|
|
6
|
+
|
|
7
|
+
When a mutating pattern is detected, a marker file is written to /tmp so the
|
|
8
|
+
Post-Bash hook can run 'git status' to report actual changes.
|
|
9
|
+
|
|
10
|
+
Marker file: /tmp/.hgp_bash_mutating_<ppid>
|
|
11
|
+
"""
|
|
12
|
+
import json
|
|
13
|
+
import os
|
|
14
|
+
import re
|
|
15
|
+
import sys
|
|
16
|
+
|
|
17
|
+
# ── Read-only command prefixes — skip pattern matching for these ──────────────
|
|
18
|
+
_READONLY_PREFIXES = (
|
|
19
|
+
"git log",
|
|
20
|
+
"git status",
|
|
21
|
+
"git diff",
|
|
22
|
+
"git show",
|
|
23
|
+
"git branch",
|
|
24
|
+
"git tag",
|
|
25
|
+
"git remote",
|
|
26
|
+
"git fetch",
|
|
27
|
+
"git ls",
|
|
28
|
+
"git stash list",
|
|
29
|
+
"ls ",
|
|
30
|
+
"ls\t",
|
|
31
|
+
"head ",
|
|
32
|
+
"tail ",
|
|
33
|
+
"grep ",
|
|
34
|
+
"rg ",
|
|
35
|
+
"find ",
|
|
36
|
+
"wc ",
|
|
37
|
+
"diff ",
|
|
38
|
+
"less ",
|
|
39
|
+
"more ",
|
|
40
|
+
"file ",
|
|
41
|
+
"stat ",
|
|
42
|
+
"pwd",
|
|
43
|
+
"date",
|
|
44
|
+
"which ",
|
|
45
|
+
"type ",
|
|
46
|
+
"uname",
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
# ── Mutating patterns (regex) ─────────────────────────────────────────────────
|
|
50
|
+
_HIGH_PATTERNS = [
|
|
51
|
+
re.compile(r"\bcp\b"),
|
|
52
|
+
re.compile(r"\bmv\b"),
|
|
53
|
+
re.compile(r"\brm\b"),
|
|
54
|
+
re.compile(r"\btee\b"),
|
|
55
|
+
re.compile(r"\btouch\b"),
|
|
56
|
+
re.compile(r"\binstall\b"),
|
|
57
|
+
re.compile(r"\bmkdir\b"),
|
|
58
|
+
re.compile(r"\brmdir\b"),
|
|
59
|
+
re.compile(r"\bchmod\b"),
|
|
60
|
+
re.compile(r"\bchown\b"),
|
|
61
|
+
re.compile(r"\bln\b"),
|
|
62
|
+
re.compile(r"\btruncate\b"),
|
|
63
|
+
# git commands that rewrite working-tree files
|
|
64
|
+
re.compile(r"\bgit\s+checkout\b"),
|
|
65
|
+
re.compile(r"\bgit\s+restore\b"),
|
|
66
|
+
re.compile(r"\bgit\s+switch\b"),
|
|
67
|
+
re.compile(r"\bgit\s+apply\b"),
|
|
68
|
+
re.compile(r"\bgit\s+revert\b"),
|
|
69
|
+
re.compile(r"\bgit\s+merge\b"),
|
|
70
|
+
re.compile(r"\bgit\s+rebase\b"),
|
|
71
|
+
re.compile(r"\bgit\s+reset\b"),
|
|
72
|
+
# patch tools
|
|
73
|
+
re.compile(r"\bpatch\b"),
|
|
74
|
+
]
|
|
75
|
+
|
|
76
|
+
_MEDIUM_PATTERNS = [
|
|
77
|
+
re.compile(r"(?<![|&])\s*>(?!>)"), # stdout redirect (not >>)
|
|
78
|
+
re.compile(r">>"), # append redirect
|
|
79
|
+
re.compile(r"\bsed\s+-i\b"),
|
|
80
|
+
re.compile(r"\bdd\b"),
|
|
81
|
+
re.compile(r"\bawk\b.*>"), # awk with redirect
|
|
82
|
+
]
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _is_readonly(command: str) -> bool:
|
|
86
|
+
stripped = command.lstrip()
|
|
87
|
+
return any(stripped.startswith(p) for p in _READONLY_PREFIXES)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _detect_mutating(command: str) -> str | None:
|
|
91
|
+
"""Return the first matched pattern description, or None if command looks safe."""
|
|
92
|
+
for pat in _HIGH_PATTERNS:
|
|
93
|
+
if pat.search(command):
|
|
94
|
+
return pat.pattern
|
|
95
|
+
for pat in _MEDIUM_PATTERNS:
|
|
96
|
+
if pat.search(command):
|
|
97
|
+
return pat.pattern
|
|
98
|
+
return None
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _marker_path() -> str:
|
|
102
|
+
return f"/tmp/.hgp_bash_mutating_{os.getppid()}"
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def main() -> None:
|
|
106
|
+
try:
|
|
107
|
+
event = json.loads(sys.stdin.read())
|
|
108
|
+
except (json.JSONDecodeError, ValueError):
|
|
109
|
+
sys.exit(0)
|
|
110
|
+
|
|
111
|
+
if event.get("tool_name") != "Bash":
|
|
112
|
+
sys.exit(0)
|
|
113
|
+
|
|
114
|
+
command: str = event.get("tool_input", {}).get("command", "")
|
|
115
|
+
if not command or _is_readonly(command):
|
|
116
|
+
sys.exit(0)
|
|
117
|
+
|
|
118
|
+
matched = _detect_mutating(command)
|
|
119
|
+
if matched is None:
|
|
120
|
+
sys.exit(0)
|
|
121
|
+
|
|
122
|
+
# Write marker so Post-Bash hook knows to run git status
|
|
123
|
+
try:
|
|
124
|
+
open(_marker_path(), "w").close()
|
|
125
|
+
except OSError:
|
|
126
|
+
pass # /tmp not writable — skip gating, hook still warns
|
|
127
|
+
|
|
128
|
+
print(
|
|
129
|
+
f"[HGP] Bash command may mutate files (matched: {matched!r}). "
|
|
130
|
+
"If this writes or deletes tracked files, prefer hgp_* tools so the "
|
|
131
|
+
"operation is recorded in HGP history.",
|
|
132
|
+
file=sys.stderr,
|
|
133
|
+
)
|
|
134
|
+
sys.exit(0)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
if __name__ == "__main__":
|
|
138
|
+
main()
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""PreToolUse hook: warn when native Write/Edit is used instead of hgp_* tools.
|
|
2
|
+
|
|
3
|
+
Exit 0 = allow the tool call (non-blocking by default).
|
|
4
|
+
Print to stderr = message shown to the agent as a warning.
|
|
5
|
+
Set HGP_HOOK_BLOCK=1 to make the hook reject native file tool calls.
|
|
6
|
+
"""
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
import sys
|
|
10
|
+
|
|
11
|
+
HGP_TOOLS = {
|
|
12
|
+
"Write": "hgp_write_file",
|
|
13
|
+
"Edit": "hgp_edit_file",
|
|
14
|
+
"MultiEdit": "hgp_edit_file",
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
BLOCK_MODE = os.environ.get("HGP_HOOK_BLOCK", "0") == "1"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def main() -> None:
|
|
21
|
+
try:
|
|
22
|
+
event = json.loads(sys.stdin.read())
|
|
23
|
+
except json.JSONDecodeError:
|
|
24
|
+
sys.exit(0)
|
|
25
|
+
|
|
26
|
+
tool_name = event.get("tool_name", "")
|
|
27
|
+
if tool_name not in HGP_TOOLS:
|
|
28
|
+
sys.exit(0)
|
|
29
|
+
|
|
30
|
+
hgp_equiv = HGP_TOOLS[tool_name]
|
|
31
|
+
msg = (
|
|
32
|
+
f"[HGP] Native `{tool_name}` detected. "
|
|
33
|
+
f"Use `{hgp_equiv}` instead to record this file operation in HGP history. "
|
|
34
|
+
f"Set HGP_HOOK_BLOCK=1 to enforce this as an error."
|
|
35
|
+
)
|
|
36
|
+
print(msg, file=sys.stderr)
|
|
37
|
+
|
|
38
|
+
if BLOCK_MODE:
|
|
39
|
+
# Exit 2 = block; Claude Code reads stderr (already printed above), ignores stdout
|
|
40
|
+
sys.exit(2)
|
|
41
|
+
|
|
42
|
+
sys.exit(0)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
if __name__ == "__main__":
|
|
46
|
+
main()
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"hooks": {
|
|
3
|
+
"PreToolUse": [
|
|
4
|
+
{
|
|
5
|
+
"matcher": "Write|Edit|MultiEdit",
|
|
6
|
+
"hooks": [
|
|
7
|
+
{
|
|
8
|
+
"type": "command",
|
|
9
|
+
"command": "python3 .claude/hooks/pre_tool_use_hgp.py"
|
|
10
|
+
}
|
|
11
|
+
]
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"matcher": "Bash",
|
|
15
|
+
"hooks": [
|
|
16
|
+
{
|
|
17
|
+
"type": "command",
|
|
18
|
+
"command": "python3 .claude/hooks/pre_bash_hgp.py"
|
|
19
|
+
}
|
|
20
|
+
]
|
|
21
|
+
}
|
|
22
|
+
],
|
|
23
|
+
"PostToolUse": [
|
|
24
|
+
{
|
|
25
|
+
"matcher": "Bash",
|
|
26
|
+
"hooks": [
|
|
27
|
+
{
|
|
28
|
+
"type": "command",
|
|
29
|
+
"command": "python3 .claude/hooks/post_bash_hgp.py"
|
|
30
|
+
}
|
|
31
|
+
]
|
|
32
|
+
}
|
|
33
|
+
]
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"permissions": {
|
|
3
|
+
"allow": [
|
|
4
|
+
"Bash(uv run:*)",
|
|
5
|
+
"Bash(git:*)",
|
|
6
|
+
"Bash(python -m pytest tests/test_file_ops.py tests/test_project.py tests/test_db.py -v --tb=short)",
|
|
7
|
+
"WebSearch",
|
|
8
|
+
"WebFetch(domain:geminicli.com)",
|
|
9
|
+
"WebFetch(domain:github.com)",
|
|
10
|
+
"mcp__plugin_context7_context7__resolve-library-id",
|
|
11
|
+
"mcp__plugin_context7_context7__query-docs"
|
|
12
|
+
]
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""AfterTool hook for Gemini CLI: detect and report actual file changes after shell commands.
|
|
2
|
+
|
|
3
|
+
Only runs git status when the BeforeTool hook wrote a marker file indicating a
|
|
4
|
+
potentially mutating command was about to execute. This avoids the overhead of
|
|
5
|
+
git status on every read-only shell call.
|
|
6
|
+
|
|
7
|
+
Gemini CLI protocol (always exit 0):
|
|
8
|
+
Report: stdout JSON {"systemMessage": "..."}
|
|
9
|
+
Pass-through: no stdout output
|
|
10
|
+
|
|
11
|
+
Known limitation: .gitignore'd files won't appear in the report.
|
|
12
|
+
|
|
13
|
+
Marker file: /tmp/.hgp_bash_mutating_<ppid>
|
|
14
|
+
"""
|
|
15
|
+
import json
|
|
16
|
+
import os
|
|
17
|
+
import subprocess
|
|
18
|
+
import sys
|
|
19
|
+
|
|
20
|
+
_TIMEOUT_SECS = 2
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _marker_path() -> str:
|
|
24
|
+
return f"/tmp/.hgp_bash_mutating_{os.getppid()}"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _consume_marker() -> bool:
|
|
28
|
+
"""Return True and remove marker if it exists, False otherwise."""
|
|
29
|
+
path = _marker_path()
|
|
30
|
+
try:
|
|
31
|
+
os.unlink(path)
|
|
32
|
+
return True
|
|
33
|
+
except FileNotFoundError:
|
|
34
|
+
return False
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _git_changed_files(cwd: str) -> list[str]:
|
|
38
|
+
"""Run git status --porcelain and return list of changed file entries."""
|
|
39
|
+
try:
|
|
40
|
+
result = subprocess.run(
|
|
41
|
+
["git", "status", "--porcelain"],
|
|
42
|
+
cwd=cwd,
|
|
43
|
+
capture_output=True,
|
|
44
|
+
text=True,
|
|
45
|
+
timeout=_TIMEOUT_SECS,
|
|
46
|
+
)
|
|
47
|
+
if result.returncode != 0:
|
|
48
|
+
return []
|
|
49
|
+
lines = [l for l in result.stdout.splitlines() if l.strip()]
|
|
50
|
+
return lines
|
|
51
|
+
except (subprocess.TimeoutExpired, FileNotFoundError, OSError):
|
|
52
|
+
return []
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def main() -> None:
|
|
56
|
+
try:
|
|
57
|
+
event = json.loads(sys.stdin.read())
|
|
58
|
+
except (json.JSONDecodeError, ValueError):
|
|
59
|
+
sys.exit(0)
|
|
60
|
+
|
|
61
|
+
if event.get("tool_name") != "shell":
|
|
62
|
+
sys.exit(0)
|
|
63
|
+
|
|
64
|
+
if not _consume_marker():
|
|
65
|
+
# No marker → BeforeTool hook didn't flag this as mutating; skip git status
|
|
66
|
+
sys.exit(0)
|
|
67
|
+
|
|
68
|
+
cwd = os.getcwd()
|
|
69
|
+
changed = _git_changed_files(cwd)
|
|
70
|
+
if not changed:
|
|
71
|
+
sys.exit(0)
|
|
72
|
+
|
|
73
|
+
lines_str = "\n ".join(changed)
|
|
74
|
+
msg = (
|
|
75
|
+
f"[HGP] Bash command changed tracked files (use hgp_* tools for history):\n {lines_str}"
|
|
76
|
+
)
|
|
77
|
+
print(json.dumps({"systemMessage": msg}))
|
|
78
|
+
sys.exit(0)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
if __name__ == "__main__":
|
|
82
|
+
main()
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"""BeforeTool hook for Gemini CLI: warn when Bash commands may mutate files outside HGP.
|
|
2
|
+
|
|
3
|
+
Gemini CLI protocol (always exit 0):
|
|
4
|
+
Warn: stdout JSON {"systemMessage": "..."}
|
|
5
|
+
Pass-through: no stdout output
|
|
6
|
+
|
|
7
|
+
When a mutating pattern is detected, a marker file is written to /tmp so the
|
|
8
|
+
AfterTool hook can run 'git status' to report actual changes.
|
|
9
|
+
|
|
10
|
+
Marker file: /tmp/.hgp_bash_mutating_<ppid>
|
|
11
|
+
"""
|
|
12
|
+
import json
|
|
13
|
+
import os
|
|
14
|
+
import re
|
|
15
|
+
import sys
|
|
16
|
+
|
|
17
|
+
# ── Read-only command prefixes — skip pattern matching for these ──────────────
|
|
18
|
+
_READONLY_PREFIXES = (
|
|
19
|
+
"git log",
|
|
20
|
+
"git status",
|
|
21
|
+
"git diff",
|
|
22
|
+
"git show",
|
|
23
|
+
"git branch",
|
|
24
|
+
"git tag",
|
|
25
|
+
"git remote",
|
|
26
|
+
"git fetch",
|
|
27
|
+
"git ls",
|
|
28
|
+
"git stash list",
|
|
29
|
+
"ls ",
|
|
30
|
+
"ls\t",
|
|
31
|
+
"head ",
|
|
32
|
+
"tail ",
|
|
33
|
+
"grep ",
|
|
34
|
+
"rg ",
|
|
35
|
+
"find ",
|
|
36
|
+
"wc ",
|
|
37
|
+
"diff ",
|
|
38
|
+
"less ",
|
|
39
|
+
"more ",
|
|
40
|
+
"file ",
|
|
41
|
+
"stat ",
|
|
42
|
+
"pwd",
|
|
43
|
+
"date",
|
|
44
|
+
"which ",
|
|
45
|
+
"type ",
|
|
46
|
+
"uname",
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
# ── Mutating patterns (regex) ─────────────────────────────────────────────────
|
|
50
|
+
_HIGH_PATTERNS = [
|
|
51
|
+
re.compile(r"\bcp\b"),
|
|
52
|
+
re.compile(r"\bmv\b"),
|
|
53
|
+
re.compile(r"\brm\b"),
|
|
54
|
+
re.compile(r"\btee\b"),
|
|
55
|
+
re.compile(r"\btouch\b"),
|
|
56
|
+
re.compile(r"\binstall\b"),
|
|
57
|
+
re.compile(r"\bmkdir\b"),
|
|
58
|
+
re.compile(r"\brmdir\b"),
|
|
59
|
+
re.compile(r"\bchmod\b"),
|
|
60
|
+
re.compile(r"\bchown\b"),
|
|
61
|
+
re.compile(r"\bln\b"),
|
|
62
|
+
re.compile(r"\btruncate\b"),
|
|
63
|
+
# git commands that rewrite working-tree files
|
|
64
|
+
re.compile(r"\bgit\s+checkout\b"),
|
|
65
|
+
re.compile(r"\bgit\s+restore\b"),
|
|
66
|
+
re.compile(r"\bgit\s+switch\b"),
|
|
67
|
+
re.compile(r"\bgit\s+apply\b"),
|
|
68
|
+
re.compile(r"\bgit\s+revert\b"),
|
|
69
|
+
re.compile(r"\bgit\s+merge\b"),
|
|
70
|
+
re.compile(r"\bgit\s+rebase\b"),
|
|
71
|
+
re.compile(r"\bgit\s+reset\b"),
|
|
72
|
+
# patch tools
|
|
73
|
+
re.compile(r"\bpatch\b"),
|
|
74
|
+
]
|
|
75
|
+
|
|
76
|
+
_MEDIUM_PATTERNS = [
|
|
77
|
+
re.compile(r"(?<![|&])\s*>(?!>)"), # stdout redirect (not >>)
|
|
78
|
+
re.compile(r">>"), # append redirect
|
|
79
|
+
re.compile(r"\bsed\s+-i\b"),
|
|
80
|
+
re.compile(r"\bdd\b"),
|
|
81
|
+
re.compile(r"\bawk\b.*>"), # awk with redirect
|
|
82
|
+
]
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _is_readonly(command: str) -> bool:
|
|
86
|
+
stripped = command.lstrip()
|
|
87
|
+
return any(stripped.startswith(p) for p in _READONLY_PREFIXES)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _detect_mutating(command: str) -> str | None:
|
|
91
|
+
"""Return the first matched pattern description, or None if command looks safe."""
|
|
92
|
+
for pat in _HIGH_PATTERNS:
|
|
93
|
+
if pat.search(command):
|
|
94
|
+
return pat.pattern
|
|
95
|
+
for pat in _MEDIUM_PATTERNS:
|
|
96
|
+
if pat.search(command):
|
|
97
|
+
return pat.pattern
|
|
98
|
+
return None
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _marker_path() -> str:
|
|
102
|
+
return f"/tmp/.hgp_bash_mutating_{os.getppid()}"
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def main() -> None:
|
|
106
|
+
try:
|
|
107
|
+
event = json.loads(sys.stdin.read())
|
|
108
|
+
except (json.JSONDecodeError, ValueError):
|
|
109
|
+
sys.exit(0)
|
|
110
|
+
|
|
111
|
+
if event.get("tool_name") != "shell":
|
|
112
|
+
sys.exit(0)
|
|
113
|
+
|
|
114
|
+
command: str = event.get("tool_input", {}).get("command", "")
|
|
115
|
+
if not command or _is_readonly(command):
|
|
116
|
+
sys.exit(0)
|
|
117
|
+
|
|
118
|
+
matched = _detect_mutating(command)
|
|
119
|
+
if matched is None:
|
|
120
|
+
sys.exit(0)
|
|
121
|
+
|
|
122
|
+
# Write marker so AfterTool hook knows to run git status
|
|
123
|
+
try:
|
|
124
|
+
open(_marker_path(), "w").close()
|
|
125
|
+
except OSError:
|
|
126
|
+
pass # /tmp not writable — skip gating, hook still warns
|
|
127
|
+
|
|
128
|
+
msg = (
|
|
129
|
+
f"[HGP] Bash command may mutate files (matched: {matched!r}). "
|
|
130
|
+
"If this writes or deletes tracked files, prefer hgp_* tools so the "
|
|
131
|
+
"operation is recorded in HGP history."
|
|
132
|
+
)
|
|
133
|
+
print(json.dumps({"systemMessage": msg}))
|
|
134
|
+
sys.exit(0)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
if __name__ == "__main__":
|
|
138
|
+
main()
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""BeforeTool hook for Gemini CLI: warn/block when native file tools are used.
|
|
2
|
+
|
|
3
|
+
Gemini CLI protocol (all responses exit 0):
|
|
4
|
+
Warn mode (default): stdout JSON {"systemMessage": "..."}
|
|
5
|
+
Block mode (HGP_HOOK_BLOCK=1): stdout JSON {"decision": "deny", "reason": "..."}
|
|
6
|
+
Pass-through: no stdout output
|
|
7
|
+
|
|
8
|
+
Set HGP_HOOK_BLOCK=1 to enforce blocking instead of warning.
|
|
9
|
+
"""
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
import sys
|
|
13
|
+
|
|
14
|
+
HGP_TOOLS = {
|
|
15
|
+
"write_file": "hgp_write_file",
|
|
16
|
+
"replace": "hgp_edit_file",
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
BLOCK_MODE = os.environ.get("HGP_HOOK_BLOCK", "0") == "1"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def main() -> None:
|
|
23
|
+
try:
|
|
24
|
+
event = json.loads(sys.stdin.read())
|
|
25
|
+
except json.JSONDecodeError:
|
|
26
|
+
sys.exit(0)
|
|
27
|
+
|
|
28
|
+
tool_name = event.get("tool_name", "")
|
|
29
|
+
if tool_name not in HGP_TOOLS:
|
|
30
|
+
sys.exit(0)
|
|
31
|
+
|
|
32
|
+
hgp_equiv = HGP_TOOLS[tool_name]
|
|
33
|
+
msg = (
|
|
34
|
+
f"[HGP] Native `{tool_name}` detected. "
|
|
35
|
+
f"Use `{hgp_equiv}` instead to record this file operation in HGP history. "
|
|
36
|
+
f"Set HGP_HOOK_BLOCK=1 to enforce this as an error."
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
if BLOCK_MODE:
|
|
40
|
+
print(json.dumps({"decision": "deny", "reason": msg}))
|
|
41
|
+
else:
|
|
42
|
+
print(json.dumps({"systemMessage": msg}))
|
|
43
|
+
|
|
44
|
+
sys.exit(0)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
if __name__ == "__main__":
|
|
48
|
+
main()
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"hooks": {
|
|
3
|
+
"BeforeTool": [
|
|
4
|
+
{
|
|
5
|
+
"matcher": "^(write_file|replace)$",
|
|
6
|
+
"hooks": [
|
|
7
|
+
{
|
|
8
|
+
"name": "hgp-enforcement",
|
|
9
|
+
"type": "command",
|
|
10
|
+
"command": "python3 .gemini/hooks/pre_tool_use_hgp.py"
|
|
11
|
+
}
|
|
12
|
+
]
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"matcher": "^shell$",
|
|
16
|
+
"hooks": [
|
|
17
|
+
{
|
|
18
|
+
"name": "hgp-bash-pre",
|
|
19
|
+
"type": "command",
|
|
20
|
+
"command": "python3 .gemini/hooks/pre_bash_hgp.py"
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
}
|
|
24
|
+
],
|
|
25
|
+
"AfterTool": [
|
|
26
|
+
{
|
|
27
|
+
"matcher": "^shell$",
|
|
28
|
+
"hooks": [
|
|
29
|
+
{
|
|
30
|
+
"name": "hgp-bash-post",
|
|
31
|
+
"type": "command",
|
|
32
|
+
"command": "python3 .gemini/hooks/post_bash_hgp.py"
|
|
33
|
+
}
|
|
34
|
+
]
|
|
35
|
+
}
|
|
36
|
+
]
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.12
|