hiro-agent 0.1.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.
- hiro_agent-0.1.0/.gitignore +7 -0
- hiro_agent-0.1.0/LICENSE +21 -0
- hiro_agent-0.1.0/PKG-INFO +81 -0
- hiro_agent-0.1.0/README.md +66 -0
- hiro_agent-0.1.0/pyproject.toml +29 -0
- hiro_agent-0.1.0/src/hiro_agent/__init__.py +3 -0
- hiro_agent-0.1.0/src/hiro_agent/_common.py +104 -0
- hiro_agent-0.1.0/src/hiro_agent/cli.py +111 -0
- hiro_agent-0.1.0/src/hiro_agent/hooks/__init__.py +0 -0
- hiro_agent-0.1.0/src/hiro_agent/hooks/enforce_code_review.py +151 -0
- hiro_agent-0.1.0/src/hiro_agent/hooks/enforce_plan_review.py +133 -0
- hiro_agent-0.1.0/src/hiro_agent/prompts.py +118 -0
- hiro_agent-0.1.0/src/hiro_agent/review_code.py +63 -0
- hiro_agent-0.1.0/src/hiro_agent/review_infra.py +117 -0
- hiro_agent-0.1.0/src/hiro_agent/review_plan.py +59 -0
- hiro_agent-0.1.0/src/hiro_agent/setup_hooks.py +376 -0
- hiro_agent-0.1.0/tests/__init__.py +0 -0
- hiro_agent-0.1.0/tests/test_common.py +171 -0
- hiro_agent-0.1.0/tests/test_prompts.py +82 -0
- hiro_agent-0.1.0/tests/test_review_code.py +87 -0
- hiro_agent-0.1.0/tests/test_review_infra.py +119 -0
- hiro_agent-0.1.0/tests/test_review_plan.py +87 -0
- hiro_agent-0.1.0/tests/test_setup_hooks.py +215 -0
hiro_agent-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Hiro Security, Inc.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: hiro-agent
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: AI security review agent for code, plans, and infrastructure
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Requires-Python: >=3.11
|
|
8
|
+
Requires-Dist: claude-agent-sdk>=0.1.37
|
|
9
|
+
Requires-Dist: click>=8.0
|
|
10
|
+
Requires-Dist: structlog
|
|
11
|
+
Provides-Extra: dev
|
|
12
|
+
Requires-Dist: pytest; extra == 'dev'
|
|
13
|
+
Requires-Dist: pytest-asyncio; extra == 'dev'
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
|
|
16
|
+
# hiro-agent
|
|
17
|
+
|
|
18
|
+
AI security review agent for code, plans, and infrastructure. Integrates with Claude Code, Cursor, VSCode Copilot, and Codex CLI to enforce security reviews before commits and plan finalization.
|
|
19
|
+
|
|
20
|
+
## Install
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pip install hiro-agent
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Quick Start
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
# Set up hooks for your AI coding tools
|
|
30
|
+
hiro setup
|
|
31
|
+
|
|
32
|
+
# Review code changes
|
|
33
|
+
git diff | hiro review-code
|
|
34
|
+
|
|
35
|
+
# Review an implementation plan
|
|
36
|
+
cat plan.md | hiro review-plan
|
|
37
|
+
|
|
38
|
+
# Review infrastructure configuration
|
|
39
|
+
hiro review-infra main.tf
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Commands
|
|
43
|
+
|
|
44
|
+
| Command | Description |
|
|
45
|
+
|---------|-------------|
|
|
46
|
+
| `hiro review-code` | Security review of code changes (stdin: git diff) |
|
|
47
|
+
| `hiro review-plan` | STRIDE threat model review of a plan (stdin) |
|
|
48
|
+
| `hiro review-infra` | IaC security review (file arg or stdin) |
|
|
49
|
+
| `hiro setup` | Auto-detect and configure all AI coding tools |
|
|
50
|
+
| `hiro verify` | Verify hook integrity against installed version |
|
|
51
|
+
|
|
52
|
+
### Setup Options
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
hiro setup # Auto-detect all tools
|
|
56
|
+
hiro setup --claude-code # Claude Code only
|
|
57
|
+
hiro setup --cursor # Cursor only
|
|
58
|
+
hiro setup --vscode # VSCode Copilot only
|
|
59
|
+
hiro setup --codex # Codex CLI only
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Configuration
|
|
63
|
+
|
|
64
|
+
Set `HIRO_API_KEY` to connect to the Hiro platform for organizational context (security policies, memories, org profile). Without it, reviews still run using your `ANTHROPIC_API_KEY` directly.
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
export HIRO_API_KEY=hiro_ak_... # Optional: Hiro platform context
|
|
68
|
+
export ANTHROPIC_API_KEY=sk-ant-... # Required if HIRO_API_KEY not set
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## How It Works
|
|
72
|
+
|
|
73
|
+
1. **`hiro setup`** installs hook scripts in `.hiro/hooks/` and configures your AI coding tool to call them
|
|
74
|
+
2. Hooks track file modifications and block commits until `hiro review-code` has run
|
|
75
|
+
3. Hooks track plan creation and block finalization until `hiro review-plan` has run
|
|
76
|
+
4. Review agents use `claude-agent-sdk` to spawn a Claude instance that performs the security review
|
|
77
|
+
5. When connected to Hiro (`HIRO_API_KEY`), reviews are enriched with your org's security policy, accepted risks, and architecture context
|
|
78
|
+
|
|
79
|
+
## License
|
|
80
|
+
|
|
81
|
+
MIT
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# hiro-agent
|
|
2
|
+
|
|
3
|
+
AI security review agent for code, plans, and infrastructure. Integrates with Claude Code, Cursor, VSCode Copilot, and Codex CLI to enforce security reviews before commits and plan finalization.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install hiro-agent
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
# Set up hooks for your AI coding tools
|
|
15
|
+
hiro setup
|
|
16
|
+
|
|
17
|
+
# Review code changes
|
|
18
|
+
git diff | hiro review-code
|
|
19
|
+
|
|
20
|
+
# Review an implementation plan
|
|
21
|
+
cat plan.md | hiro review-plan
|
|
22
|
+
|
|
23
|
+
# Review infrastructure configuration
|
|
24
|
+
hiro review-infra main.tf
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Commands
|
|
28
|
+
|
|
29
|
+
| Command | Description |
|
|
30
|
+
|---------|-------------|
|
|
31
|
+
| `hiro review-code` | Security review of code changes (stdin: git diff) |
|
|
32
|
+
| `hiro review-plan` | STRIDE threat model review of a plan (stdin) |
|
|
33
|
+
| `hiro review-infra` | IaC security review (file arg or stdin) |
|
|
34
|
+
| `hiro setup` | Auto-detect and configure all AI coding tools |
|
|
35
|
+
| `hiro verify` | Verify hook integrity against installed version |
|
|
36
|
+
|
|
37
|
+
### Setup Options
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
hiro setup # Auto-detect all tools
|
|
41
|
+
hiro setup --claude-code # Claude Code only
|
|
42
|
+
hiro setup --cursor # Cursor only
|
|
43
|
+
hiro setup --vscode # VSCode Copilot only
|
|
44
|
+
hiro setup --codex # Codex CLI only
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Configuration
|
|
48
|
+
|
|
49
|
+
Set `HIRO_API_KEY` to connect to the Hiro platform for organizational context (security policies, memories, org profile). Without it, reviews still run using your `ANTHROPIC_API_KEY` directly.
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
export HIRO_API_KEY=hiro_ak_... # Optional: Hiro platform context
|
|
53
|
+
export ANTHROPIC_API_KEY=sk-ant-... # Required if HIRO_API_KEY not set
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## How It Works
|
|
57
|
+
|
|
58
|
+
1. **`hiro setup`** installs hook scripts in `.hiro/hooks/` and configures your AI coding tool to call them
|
|
59
|
+
2. Hooks track file modifications and block commits until `hiro review-code` has run
|
|
60
|
+
3. Hooks track plan creation and block finalization until `hiro review-plan` has run
|
|
61
|
+
4. Review agents use `claude-agent-sdk` to spawn a Claude instance that performs the security review
|
|
62
|
+
5. When connected to Hiro (`HIRO_API_KEY`), reviews are enriched with your org's security policy, accepted risks, and architecture context
|
|
63
|
+
|
|
64
|
+
## License
|
|
65
|
+
|
|
66
|
+
MIT
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "hiro-agent"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "AI security review agent for code, plans, and infrastructure"
|
|
5
|
+
requires-python = ">=3.11"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
readme = "README.md"
|
|
8
|
+
dependencies = [
|
|
9
|
+
"claude-agent-sdk>=0.1.37",
|
|
10
|
+
"structlog",
|
|
11
|
+
"click>=8.0",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
[project.scripts]
|
|
15
|
+
hiro = "hiro_agent.cli:main"
|
|
16
|
+
|
|
17
|
+
[build-system]
|
|
18
|
+
requires = ["hatchling"]
|
|
19
|
+
build-backend = "hatchling.build"
|
|
20
|
+
|
|
21
|
+
[tool.hatch.build.targets.wheel]
|
|
22
|
+
packages = ["src/hiro_agent"]
|
|
23
|
+
|
|
24
|
+
[tool.pytest.ini_options]
|
|
25
|
+
testpaths = ["tests"]
|
|
26
|
+
asyncio_mode = "strict"
|
|
27
|
+
|
|
28
|
+
[project.optional-dependencies]
|
|
29
|
+
dev = ["pytest", "pytest-asyncio"]
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""Shared agent runner for local security review agents.
|
|
2
|
+
|
|
3
|
+
CLAUDECODE="" prevents claude-agent-sdk from detecting a nested Claude Code
|
|
4
|
+
session and rejecting the spawn. This is intentional — the review agent is
|
|
5
|
+
a separate subprocess, not a nested invocation of the caller's session.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
|
|
10
|
+
import structlog
|
|
11
|
+
from claude_agent_sdk import (
|
|
12
|
+
AssistantMessage,
|
|
13
|
+
ClaudeAgentOptions,
|
|
14
|
+
TextBlock,
|
|
15
|
+
query,
|
|
16
|
+
)
|
|
17
|
+
from claude_agent_sdk.types import McpHttpServerConfig
|
|
18
|
+
|
|
19
|
+
logger = structlog.get_logger(__name__)
|
|
20
|
+
|
|
21
|
+
# Hardcoded — not configurable to prevent SSRF. HTTPS enforced.
|
|
22
|
+
HIRO_MCP_URL = "https://api.hiro.is/mcp/architect"
|
|
23
|
+
HIRO_BACKEND_URL = "https://api.hiro.is"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _get_mcp_config() -> dict[str, McpHttpServerConfig]:
|
|
27
|
+
"""Build MCP server config for connecting to Hiro.
|
|
28
|
+
|
|
29
|
+
Returns an empty dict when HIRO_API_KEY is not set (MCP context
|
|
30
|
+
tools will be unavailable but the review still runs).
|
|
31
|
+
"""
|
|
32
|
+
key = os.environ.get("HIRO_API_KEY", "")
|
|
33
|
+
if not key:
|
|
34
|
+
return {}
|
|
35
|
+
return {
|
|
36
|
+
"hiro": McpHttpServerConfig(
|
|
37
|
+
url=HIRO_MCP_URL,
|
|
38
|
+
headers={"Authorization": f"Bearer {key}"},
|
|
39
|
+
),
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _get_agent_env() -> dict[str, str]:
|
|
44
|
+
"""Build env vars for the agent subprocess.
|
|
45
|
+
|
|
46
|
+
When HIRO_API_KEY is set, route LLM calls through the Hiro backend
|
|
47
|
+
proxy to Bedrock (keeps source code within AWS infrastructure).
|
|
48
|
+
Otherwise the agent uses the developer's ANTHROPIC_API_KEY directly.
|
|
49
|
+
"""
|
|
50
|
+
env: dict[str, str] = {"CLAUDECODE": ""}
|
|
51
|
+
api_key = os.environ.get("HIRO_API_KEY", "")
|
|
52
|
+
if api_key:
|
|
53
|
+
env["ANTHROPIC_BASE_URL"] = f"{HIRO_BACKEND_URL}/api/llm-proxy"
|
|
54
|
+
env["ANTHROPIC_API_KEY"] = api_key
|
|
55
|
+
return env
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
async def run_review_agent(
|
|
59
|
+
prompt: str,
|
|
60
|
+
system_prompt: str,
|
|
61
|
+
*,
|
|
62
|
+
cwd: str | None = None,
|
|
63
|
+
allowed_tools: list[str] | None = None,
|
|
64
|
+
max_turns: int = 15,
|
|
65
|
+
) -> str:
|
|
66
|
+
"""Run a local review agent and return its final text output.
|
|
67
|
+
|
|
68
|
+
The agent connects to the Hiro MCP server for organizational context
|
|
69
|
+
(memories, security policy, org profile) and optionally has filesystem
|
|
70
|
+
access via Read/Grep/Glob tools.
|
|
71
|
+
|
|
72
|
+
Only read-only MCP tools are allowed — remember, set_org_context, and
|
|
73
|
+
forget are explicitly excluded to prevent the review agent from
|
|
74
|
+
modifying organizational state.
|
|
75
|
+
"""
|
|
76
|
+
mcp_config = _get_mcp_config()
|
|
77
|
+
|
|
78
|
+
# MCP context tools are only available when connected to Hiro
|
|
79
|
+
mcp_tools = []
|
|
80
|
+
if mcp_config:
|
|
81
|
+
mcp_tools = [
|
|
82
|
+
"mcp__hiro__get_org_context",
|
|
83
|
+
"mcp__hiro__recall",
|
|
84
|
+
"mcp__hiro__get_security_policy",
|
|
85
|
+
]
|
|
86
|
+
|
|
87
|
+
options = ClaudeAgentOptions(
|
|
88
|
+
cwd=cwd,
|
|
89
|
+
allowed_tools=(allowed_tools or []) + mcp_tools,
|
|
90
|
+
system_prompt=system_prompt,
|
|
91
|
+
mcp_servers=mcp_config,
|
|
92
|
+
permission_mode="acceptEdits",
|
|
93
|
+
max_turns=max_turns,
|
|
94
|
+
env=_get_agent_env(),
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
summary = ""
|
|
98
|
+
async for message in query(prompt=prompt, options=options):
|
|
99
|
+
if isinstance(message, AssistantMessage):
|
|
100
|
+
for block in message.content:
|
|
101
|
+
if isinstance(block, TextBlock):
|
|
102
|
+
summary = block.text
|
|
103
|
+
|
|
104
|
+
return summary
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""CLI entry point: hiro review-code, review-plan, review-infra, setup, verify."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
import structlog
|
|
9
|
+
|
|
10
|
+
logger = structlog.get_logger(__name__)
|
|
11
|
+
|
|
12
|
+
# 2 MB stdin cap to prevent accidental piping of huge files
|
|
13
|
+
MAX_STDIN_BYTES = 2 * 1024 * 1024
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _read_stdin(command_name: str) -> str:
|
|
17
|
+
"""Read stdin with a 2MB size cap."""
|
|
18
|
+
if sys.stdin.isatty():
|
|
19
|
+
click.echo(f"Error: No input on stdin. Pipe content to `hiro {command_name}`.", err=True)
|
|
20
|
+
raise SystemExit(1)
|
|
21
|
+
data = sys.stdin.buffer.read(MAX_STDIN_BYTES + 1)
|
|
22
|
+
if len(data) > MAX_STDIN_BYTES:
|
|
23
|
+
click.echo(
|
|
24
|
+
f"Error: Input exceeds 2MB limit ({len(data)} bytes). "
|
|
25
|
+
"Reduce the input size or review files individually.",
|
|
26
|
+
err=True,
|
|
27
|
+
)
|
|
28
|
+
raise SystemExit(1)
|
|
29
|
+
return data.decode("utf-8", errors="replace")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@click.group()
|
|
33
|
+
@click.version_option(package_name="hiro-agent")
|
|
34
|
+
def main() -> None:
|
|
35
|
+
"""Hiro — AI security review agent."""
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@main.command("review-code")
|
|
39
|
+
@click.option("--context", "-c", default="", help="Additional context about the code.")
|
|
40
|
+
def review_code_cmd(context: str) -> None:
|
|
41
|
+
"""Review code changes for security issues. Reads diff from stdin."""
|
|
42
|
+
from hiro_agent.review_code import review_code
|
|
43
|
+
|
|
44
|
+
diff = _read_stdin("review-code")
|
|
45
|
+
if not diff.strip():
|
|
46
|
+
click.echo("Error: Empty input. Pipe a diff: git diff | hiro review-code", err=True)
|
|
47
|
+
raise SystemExit(1)
|
|
48
|
+
|
|
49
|
+
cwd = os.getcwd()
|
|
50
|
+
result = asyncio.run(review_code(diff, cwd=cwd, context=context))
|
|
51
|
+
click.echo(result)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@main.command("review-plan")
|
|
55
|
+
@click.option("--context", "-c", default="", help="Additional context about the plan.")
|
|
56
|
+
def review_plan_cmd(context: str) -> None:
|
|
57
|
+
"""Review an implementation plan for security concerns. Reads from stdin."""
|
|
58
|
+
from hiro_agent.review_plan import review_plan
|
|
59
|
+
|
|
60
|
+
plan = _read_stdin("review-plan")
|
|
61
|
+
if not plan.strip():
|
|
62
|
+
click.echo("Error: Empty input. Pipe a plan: cat plan.md | hiro review-plan", err=True)
|
|
63
|
+
raise SystemExit(1)
|
|
64
|
+
|
|
65
|
+
result = asyncio.run(review_plan(plan, context=context))
|
|
66
|
+
click.echo(result)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@main.command("review-infra")
|
|
70
|
+
@click.argument("filepath", required=False)
|
|
71
|
+
def review_infra_cmd(filepath: str | None) -> None:
|
|
72
|
+
"""Review infrastructure config for security issues. File arg or stdin."""
|
|
73
|
+
from hiro_agent.review_infra import review_infrastructure
|
|
74
|
+
|
|
75
|
+
if filepath:
|
|
76
|
+
filename = os.path.basename(filepath)
|
|
77
|
+
filepath = os.path.abspath(filepath)
|
|
78
|
+
cwd = os.path.dirname(filepath)
|
|
79
|
+
result = asyncio.run(
|
|
80
|
+
review_infrastructure(filepath, filename=filename, cwd=cwd)
|
|
81
|
+
)
|
|
82
|
+
else:
|
|
83
|
+
config = _read_stdin("review-infra")
|
|
84
|
+
if not config.strip():
|
|
85
|
+
click.echo("Error: Empty input. Usage: hiro review-infra <file>", err=True)
|
|
86
|
+
raise SystemExit(1)
|
|
87
|
+
result = asyncio.run(
|
|
88
|
+
review_infrastructure(config, filename="stdin")
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
click.echo(result)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@main.command()
|
|
95
|
+
@click.option("--claude-code", "tools", flag_value="claude-code", help="Claude Code only.")
|
|
96
|
+
@click.option("--cursor", "tools", flag_value="cursor", help="Cursor only.")
|
|
97
|
+
@click.option("--vscode", "tools", flag_value="vscode", help="VSCode Copilot only.")
|
|
98
|
+
@click.option("--codex", "tools", flag_value="codex", help="Codex CLI only.")
|
|
99
|
+
def setup(tools: str | None) -> None:
|
|
100
|
+
"""Configure AI coding tool hooks for security review enforcement."""
|
|
101
|
+
from hiro_agent.setup_hooks import run_setup
|
|
102
|
+
|
|
103
|
+
run_setup(tool_filter=tools)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@main.command()
|
|
107
|
+
def verify() -> None:
|
|
108
|
+
"""Verify hook integrity against installed package version."""
|
|
109
|
+
from hiro_agent.setup_hooks import run_verify
|
|
110
|
+
|
|
111
|
+
run_verify()
|
|
File without changes
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Enforce Hiro code review before committing.
|
|
3
|
+
|
|
4
|
+
Tracks whether files have been modified since the last review_code call.
|
|
5
|
+
Blocks git commit until a code review has been run on the changes.
|
|
6
|
+
|
|
7
|
+
Tool-agnostic: works with Claude Code, Cursor, VSCode Copilot, and
|
|
8
|
+
as a git pre-commit hook. Detects caller format automatically.
|
|
9
|
+
|
|
10
|
+
State stored in .hiro/.state/ (not tool-specific directories).
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
import os
|
|
17
|
+
import stat
|
|
18
|
+
import sys
|
|
19
|
+
import time
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from typing import Any
|
|
22
|
+
|
|
23
|
+
STATE_DIR = Path(".hiro/.state")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _state_path(session_id: str) -> Path:
|
|
27
|
+
safe = session_id.replace("/", "_")
|
|
28
|
+
return STATE_DIR / f"code_review_{safe}.json"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _load(session_id: str) -> dict[str, Any]:
|
|
32
|
+
path = _state_path(session_id)
|
|
33
|
+
if not path.exists():
|
|
34
|
+
return {}
|
|
35
|
+
try:
|
|
36
|
+
return json.loads(path.read_text())
|
|
37
|
+
except Exception:
|
|
38
|
+
return {}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _save(session_id: str, state: dict[str, Any]) -> None:
|
|
42
|
+
STATE_DIR.mkdir(parents=True, exist_ok=True)
|
|
43
|
+
os.chmod(STATE_DIR, stat.S_IRWXU) # 0700
|
|
44
|
+
state["updated_at"] = int(time.time())
|
|
45
|
+
path = _state_path(session_id)
|
|
46
|
+
path.write_text(json.dumps(state))
|
|
47
|
+
os.chmod(path, stat.S_IRUSR | stat.S_IWUSR) # 0600
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _allow() -> None:
|
|
51
|
+
print("{}")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _deny(message: str) -> None:
|
|
55
|
+
print(
|
|
56
|
+
json.dumps(
|
|
57
|
+
{
|
|
58
|
+
"hookSpecificOutput": {
|
|
59
|
+
"hookEventName": "PreToolUse",
|
|
60
|
+
"permissionDecision": "deny",
|
|
61
|
+
},
|
|
62
|
+
"systemMessage": message,
|
|
63
|
+
}
|
|
64
|
+
)
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _is_git_commit(command: str) -> bool:
|
|
69
|
+
"""Check if a bash command is a git commit."""
|
|
70
|
+
stripped = command.strip()
|
|
71
|
+
parts = stripped.split("&&")
|
|
72
|
+
for part in parts:
|
|
73
|
+
tokens = part.strip().split()
|
|
74
|
+
if len(tokens) >= 2 and tokens[0] == "git" and tokens[1] == "commit":
|
|
75
|
+
return True
|
|
76
|
+
return False
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _is_review_command(tool_name: str, tool_input: dict[str, Any]) -> bool:
|
|
80
|
+
"""Check if this is a code review action (MCP tool or CLI command)."""
|
|
81
|
+
if tool_name == "mcp__hiro__review_code":
|
|
82
|
+
return True
|
|
83
|
+
if tool_name == "Bash":
|
|
84
|
+
cmd = tool_input.get("command", "")
|
|
85
|
+
# Match both old (hiro_review.review_code) and new (hiro review-code) forms
|
|
86
|
+
if "hiro_review.review_code" in cmd or "hiro review-code" in cmd:
|
|
87
|
+
return True
|
|
88
|
+
return False
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def main() -> int:
|
|
92
|
+
try:
|
|
93
|
+
input_data = json.load(sys.stdin)
|
|
94
|
+
except Exception:
|
|
95
|
+
_allow()
|
|
96
|
+
return 0
|
|
97
|
+
|
|
98
|
+
event = input_data.get("hook_event_name", "")
|
|
99
|
+
session_id = input_data.get("session_id", "default")
|
|
100
|
+
tool_name = input_data.get("tool_name", "")
|
|
101
|
+
tool_input = input_data.get("tool_input", {})
|
|
102
|
+
|
|
103
|
+
# --- PostToolUse: track edits and reviews ---
|
|
104
|
+
if event == "PostToolUse":
|
|
105
|
+
if tool_name in ("Edit", "Write"):
|
|
106
|
+
state = _load(session_id)
|
|
107
|
+
files = state.get("modified_files", [])
|
|
108
|
+
file_path = tool_input.get("file_path", "")
|
|
109
|
+
if file_path and file_path not in files:
|
|
110
|
+
files.append(file_path)
|
|
111
|
+
state["modified_files"] = files
|
|
112
|
+
state["needs_review"] = True
|
|
113
|
+
_save(session_id, state)
|
|
114
|
+
|
|
115
|
+
elif _is_review_command(tool_name, tool_input):
|
|
116
|
+
# Review done — clear the flag
|
|
117
|
+
state = _load(session_id)
|
|
118
|
+
state["needs_review"] = False
|
|
119
|
+
state["modified_files"] = []
|
|
120
|
+
_save(session_id, state)
|
|
121
|
+
|
|
122
|
+
_allow()
|
|
123
|
+
return 0
|
|
124
|
+
|
|
125
|
+
# --- PreToolUse: block git commit if review is pending ---
|
|
126
|
+
if event == "PreToolUse" and tool_name == "Bash":
|
|
127
|
+
command = tool_input.get("command", "")
|
|
128
|
+
if _is_git_commit(command):
|
|
129
|
+
state = _load(session_id)
|
|
130
|
+
if state.get("needs_review"):
|
|
131
|
+
files = state.get("modified_files", [])
|
|
132
|
+
file_list = ", ".join(files[-5:])
|
|
133
|
+
if len(files) > 5:
|
|
134
|
+
file_list += f" and {len(files) - 5} more"
|
|
135
|
+
_deny(
|
|
136
|
+
f"Commit blocked: {len(files)} file(s) modified since last "
|
|
137
|
+
f"security review ({file_list}). Run "
|
|
138
|
+
"`git diff | hiro review-code` "
|
|
139
|
+
"before committing."
|
|
140
|
+
)
|
|
141
|
+
return 0
|
|
142
|
+
|
|
143
|
+
_allow()
|
|
144
|
+
return 0
|
|
145
|
+
|
|
146
|
+
_allow()
|
|
147
|
+
return 0
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
if __name__ == "__main__":
|
|
151
|
+
raise SystemExit(main())
|