Skip to content

Commit 7ad4660

Browse files
enforce output flag
1 parent d92f626 commit 7ad4660

File tree

6 files changed

+124
-7
lines changed

6 files changed

+124
-7
lines changed

.hiro/hooks/enforce_code_review.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,24 @@ def main() -> int:
155155
"to read the report."
156156
)
157157
return 0
158+
if cmd and "--output" not in cmd and "-o" not in cmd:
159+
is_plan = "review-plan" in cmd or "review_plan" in cmd
160+
if is_plan:
161+
_deny(
162+
"Missing --output flag. Use:\n"
163+
" hiro review-plan --file /path/to/plan.md "
164+
"--output .hiro/.state/plan-review.md\n"
165+
"Then use the Read tool on .hiro/.state/plan-review.md "
166+
"to read the report."
167+
)
168+
else:
169+
_deny(
170+
"Missing --output flag. Use:\n"
171+
" hiro review-code --output .hiro/.state/code-review.md\n"
172+
"Then use the Read tool on .hiro/.state/code-review.md "
173+
"to read the report."
174+
)
175+
return 0
158176

159177
# --- PreToolUse: block git commit if review is pending ---
160178
if event == "PreToolUse" and tool_name == "Bash":

src/hiro_agent/_common.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747

4848
# Hardcoded — not configurable to prevent SSRF. HTTPS enforced.
4949
HIRO_MCP_URL = "https://api.hiro.is/mcp/architect/mcp"
50+
HIRO_NOTIFICATIONS_MCP_URL = "https://api.hiro.is/mcp/notifications/mcp"
5051
HIRO_BACKEND_URL = "https://api.hiro.is"
5152

5253
_EXPLORE_AGENT = AgentDefinition(

src/hiro_agent/hooks/enforce_code_review.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,24 @@ def main() -> int:
170170
"to read the report."
171171
)
172172
return 0
173+
if cmd and "--output" not in cmd and "-o" not in cmd:
174+
is_plan = "review-plan" in cmd or "review_plan" in cmd
175+
if is_plan:
176+
_deny(
177+
"Missing --output flag. Use:\n"
178+
" hiro review-plan --file /path/to/plan.md "
179+
"--output .hiro/.state/plan-review.md\n"
180+
"Then use the Read tool on .hiro/.state/plan-review.md "
181+
"to read the report."
182+
)
183+
else:
184+
_deny(
185+
"Missing --output flag. Use:\n"
186+
" hiro review-code --output .hiro/.state/code-review.md\n"
187+
"Then use the Read tool on .hiro/.state/code-review.md "
188+
"to read the report."
189+
)
190+
return 0
173191

174192
# --- PreToolUse: block git commit if review is pending ---
175193
if event == "PreToolUse" and tool_name == "Bash":

src/hiro_agent/setup_hooks.py

Lines changed: 44 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,9 @@ def _install_hooks(project_root: Path) -> None:
8989

9090

9191
def _setup_claude_code(project_root: Path) -> None:
92-
"""Configure Claude Code hooks in .claude/settings.local.json."""
92+
"""Configure Claude Code hooks in .claude/settings.local.json and MCP servers in .mcp.json."""
93+
from hiro_agent._common import HIRO_MCP_URL, HIRO_NOTIFICATIONS_MCP_URL
94+
9395
click.echo("Configuring Claude Code...")
9496

9597
settings_dir = project_root / ".claude"
@@ -155,6 +157,38 @@ def _setup_claude_code(project_root: Path) -> None:
155157
settings_file.write_text(json.dumps(settings, indent=2) + "\n")
156158
click.echo(f" Wrote {settings_file}")
157159

160+
# Configure MCP servers in .mcp.json (merge, don't overwrite)
161+
api_key = _resolve_api_key(project_root)
162+
if not api_key:
163+
click.echo(" Skipping .mcp.json: No API key found.")
164+
return
165+
166+
mcp_file = project_root / ".mcp.json"
167+
if mcp_file.exists():
168+
try:
169+
mcp_config = json.loads(mcp_file.read_text())
170+
except (json.JSONDecodeError, OSError):
171+
mcp_config = {}
172+
else:
173+
mcp_config = {}
174+
175+
mcp_servers = mcp_config.setdefault("mcpServers", {})
176+
auth_headers = {"Authorization": f"Bearer {api_key}"}
177+
178+
mcp_servers["hiro"] = {
179+
"type": "http",
180+
"url": HIRO_MCP_URL,
181+
"headers": auth_headers,
182+
}
183+
mcp_servers["hiro-findings"] = {
184+
"type": "http",
185+
"url": HIRO_NOTIFICATIONS_MCP_URL,
186+
"headers": auth_headers,
187+
}
188+
189+
mcp_file.write_text(json.dumps(mcp_config, indent=2) + "\n")
190+
click.echo(f" Wrote {mcp_file}")
191+
158192

159193
def _setup_cursor(project_root: Path) -> None:
160194
"""Configure Cursor hooks in .cursor/hooks.json."""
@@ -313,8 +347,8 @@ def _resolve_api_key(project_root: Path) -> str | None:
313347

314348

315349
def _setup_claude_desktop(project_root: Path) -> None:
316-
"""Configure Claude Desktop MCP server entry in the global config."""
317-
from hiro_agent._common import HIRO_MCP_URL
350+
"""Configure Claude Desktop MCP server entries in the global config."""
351+
from hiro_agent._common import HIRO_MCP_URL, HIRO_NOTIFICATIONS_MCP_URL
318352

319353
click.echo("Configuring Claude Desktop...")
320354

@@ -337,11 +371,15 @@ def _setup_claude_desktop(project_root: Path) -> None:
337371
config = {}
338372

339373
mcp_servers = config.setdefault("mcpServers", {})
374+
auth_headers = {"Authorization": f"Bearer {api_key}"}
375+
340376
mcp_servers["hiro"] = {
341377
"url": HIRO_MCP_URL,
342-
"headers": {
343-
"Authorization": f"Bearer {api_key}",
344-
},
378+
"headers": auth_headers,
379+
}
380+
mcp_servers["hiro-findings"] = {
381+
"url": HIRO_NOTIFICATIONS_MCP_URL,
382+
"headers": auth_headers,
345383
}
346384

347385
config_path.parent.mkdir(parents=True, exist_ok=True)

tests/test_setup_hooks.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,45 @@ def test_preserves_existing_settings(self, tmp_path: Path):
159159
assert settings["permissions"]["allow"] == ["Bash"]
160160
assert "hooks" in settings
161161

162+
def test_writes_mcp_json_with_api_key(self, tmp_path: Path):
163+
(tmp_path / ".hiro").mkdir()
164+
(tmp_path / ".hiro" / "config.json").write_text(json.dumps({"api_key": "test-key"}))
165+
166+
_setup_claude_code(tmp_path)
167+
168+
mcp_file = tmp_path / ".mcp.json"
169+
assert mcp_file.exists()
170+
config = json.loads(mcp_file.read_text())
171+
assert "hiro" in config["mcpServers"]
172+
assert "hiro-findings" in config["mcpServers"]
173+
assert config["mcpServers"]["hiro"]["type"] == "http"
174+
assert config["mcpServers"]["hiro"]["url"] == "https://api.hiro.is/mcp/architect/mcp"
175+
assert config["mcpServers"]["hiro-findings"]["url"] == "https://api.hiro.is/mcp/notifications/mcp"
176+
assert config["mcpServers"]["hiro"]["headers"]["Authorization"] == "Bearer test-key"
177+
178+
def test_mcp_json_preserves_existing_servers(self, tmp_path: Path):
179+
(tmp_path / ".hiro").mkdir()
180+
(tmp_path / ".hiro" / "config.json").write_text(json.dumps({"api_key": "test-key"}))
181+
(tmp_path / ".mcp.json").write_text(json.dumps({
182+
"mcpServers": {
183+
"my-custom-server": {"type": "http", "url": "https://example.com/mcp"},
184+
}
185+
}))
186+
187+
_setup_claude_code(tmp_path)
188+
189+
config = json.loads((tmp_path / ".mcp.json").read_text())
190+
assert "my-custom-server" in config["mcpServers"]
191+
assert "hiro" in config["mcpServers"]
192+
assert "hiro-findings" in config["mcpServers"]
193+
194+
def test_skips_mcp_json_without_api_key(self, tmp_path: Path):
195+
with patch.dict(os.environ, {}, clear=False):
196+
os.environ.pop("HIRO_API_KEY", None)
197+
_setup_claude_code(tmp_path)
198+
199+
assert not (tmp_path / ".mcp.json").exists()
200+
162201

163202
class TestSetupCursor:
164203
"""Test Cursor hook configuration."""
@@ -208,8 +247,11 @@ def test_writes_config_file(self, tmp_path: Path):
208247
config = json.loads(config_path.read_text())
209248
assert "mcpServers" in config
210249
assert "hiro" in config["mcpServers"]
250+
assert "hiro-findings" in config["mcpServers"]
211251
assert config["mcpServers"]["hiro"]["url"] == "https://api.hiro.is/mcp/architect/mcp"
252+
assert config["mcpServers"]["hiro-findings"]["url"] == "https://api.hiro.is/mcp/notifications/mcp"
212253
assert config["mcpServers"]["hiro"]["headers"]["Authorization"] == "Bearer test-key-123"
254+
assert config["mcpServers"]["hiro-findings"]["headers"]["Authorization"] == "Bearer test-key-123"
213255

214256
def test_preserves_existing_mcp_servers(self, tmp_path: Path):
215257
config_path = tmp_path / "Claude" / "claude_desktop_config.json"

uv.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)