Skip to content

Commit 001b352

Browse files
authored
fix: enforce no-auto-install constraint in non-interactive mode (issue #7)
fix: enforce no-auto-install constraint in non-interactive mode (issue #7)
2 parents c40ec11 + afa705c commit 001b352

3 files changed

Lines changed: 70 additions & 5 deletions

File tree

‎bin/hunt‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
#!/usr/bin/env bash
22
# hunt — run the full agent-hunter pipeline (context → GitHub → scan → score → report)
3-
# Usage: hunt [project_root] [--intent "intent string"]
3+
# Usage: hunt [project_root] [--intent "intent string"] [--yes]
44
# Output: scored, security-scanned hunt report to stdout
55
SKILL_DIR="$(cd "$(dirname "$0")/.." && pwd)"
66
exec "$SKILL_DIR/.venv/bin/python" "$SKILL_DIR/scripts/main.py" hunt "${@:-"."}"

‎scripts/main.py‎

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -165,9 +165,9 @@ def _prompt_confirm_actions(
165165
print(" above before confirming. You can remove any from the list.")
166166
print("\n" + "─" * 70)
167167

168-
# --yes flag or non-interactive stdin: auto-confirm all
169-
if auto_yes or not sys.stdin.isatty():
170-
print(" Auto-confirmed (--yes or non-interactive mode).")
168+
# --yes flag: auto-confirm all
169+
if auto_yes:
170+
print(" Auto-confirmed (--yes).")
171171
return actions
172172

173173
# Get user input
@@ -201,8 +201,11 @@ def _prompt_confirm_actions(
201201
def cmd_hunt(args: list[str]) -> int:
202202
"""Run the full hunt pipeline.
203203
204+
Default (no flags): prints report and recommended install commands, exits 0.
205+
Non-interactive mode (piped/scripted) without --yes also exits without executing.
206+
204207
Flags:
205-
--yes Skip confirmation prompt and execute all actions.
208+
--yes Execute all recommended install/disable actions.
206209
--print-actions Print pending actions as JSON to stdout, then exit 0.
207210
SKILL.md reads this output to present confirmation in chat.
208211
@@ -255,6 +258,10 @@ def cmd_hunt(args: list[str]) -> int:
255258
return 1
256259

257260
print(f"[agent-hunter] Stack: {', '.join(profile.tech_stack[:8])}")
261+
print(
262+
"[agent-hunter] Privacy: only framework/library names extracted — "
263+
"no file paths, variable names, or project-specific code."
264+
)
258265

259266
# --- Hunt GitHub ---
260267
github_token = os.environ.get("GITHUB_TOKEN")
@@ -365,6 +372,15 @@ def cmd_hunt(args: list[str]) -> int:
365372
print(json.dumps(output, indent=2))
366373
return 0
367374

375+
# Non-interactive without --yes: dry-run only. Print commands, do not execute.
376+
# Satisfies SKILL.md §Core Constraints: "No auto-install. Show the command, user runs it."
377+
if not yes and not sys.stdin.isatty():
378+
print("\n[agent-hunter] Non-interactive mode — pass --yes to execute:")
379+
for a in actions:
380+
if a.action == "install":
381+
print(f" git clone {a.repo_url} ~/.claude/skills/{a.skill_name}")
382+
return 0
383+
368384
confirmed_actions = _prompt_confirm_actions(actions, auto_yes=yes)
369385

370386
if not confirmed_actions:

‎tests/test_main.py‎

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1495,3 +1495,52 @@ def test_yes_and_print_actions_together_prefers_print_actions(
14951495
# --print-actions exits before execute_actions
14961496
mi.return_value.execute_actions.assert_not_called()
14971497
assert code == 0
1498+
1499+
def test_noninteractive_without_yes_does_not_execute(self, tmp_path, monkeypatch, capsys):
1500+
"""Non-interactive mode without --yes: dry-run only, execute_actions NOT called."""
1501+
mock_result, mock_scored, mock_scan, mock_action = self._setup_mocks(tmp_path, monkeypatch)
1502+
1503+
monkeypatch.setattr("sys.stdin.isatty", lambda: False)
1504+
1505+
with (
1506+
patch("main.Hunter") as mh,
1507+
patch("main.scan_skill") as ms,
1508+
patch("main.score_results") as msc,
1509+
patch("main.render_hunt_report"),
1510+
patch("main.build_action_list", return_value=[mock_action]),
1511+
patch("main._list_installed_skills", return_value=set()),
1512+
patch("main._get_dangerous_installed", return_value=[]),
1513+
patch("main.Installer") as mi,
1514+
):
1515+
mh.return_value.hunt.return_value = [mock_result]
1516+
ms.return_value = mock_scan
1517+
msc.return_value = [mock_scored]
1518+
code = run(["hunt", str(tmp_path)])
1519+
1520+
assert code == 0, "dry-run mode should exit 0"
1521+
mi.return_value.execute_actions.assert_not_called()
1522+
out = capsys.readouterr().out
1523+
assert "--yes" in out, "should show --yes hint"
1524+
1525+
def test_privacy_prompt_emitted(self, tmp_path, monkeypatch, capsys):
1526+
"""Privacy notice should appear in output before hunting."""
1527+
mock_result, mock_scored, mock_scan, mock_action = self._setup_mocks(tmp_path, monkeypatch)
1528+
1529+
monkeypatch.setattr("sys.stdin.isatty", lambda: False)
1530+
1531+
with (
1532+
patch("main.Hunter") as mh,
1533+
patch("main.scan_skill") as ms,
1534+
patch("main.score_results") as msc,
1535+
patch("main.render_hunt_report"),
1536+
patch("main.build_action_list", return_value=[mock_action]),
1537+
patch("main._list_installed_skills", return_value=set()),
1538+
patch("main._get_dangerous_installed", return_value=[]),
1539+
):
1540+
mh.return_value.hunt.return_value = [mock_result]
1541+
ms.return_value = mock_scan
1542+
msc.return_value = [mock_scored]
1543+
run(["hunt", str(tmp_path)])
1544+
1545+
out = capsys.readouterr().out
1546+
assert "Privacy" in out, "privacy notice must appear in output"

0 commit comments

Comments
 (0)