Skip to content

Commit 8ad3fe1

Browse files
SafetyMPSage Hart
andauthored
chore: portfolio audit — harness, verify.sh, GHCR typo (#30)
* chore: harness solo scaffold and doc fixes for portfolio audit Portfolio documentation audit (2026-07-12): harness profile, verify.sh, command-first AGENTS.md, GHCR org typo fix, and README corrections. * style: format harness profile for Prettier lint Fixes CI format:check failure on docs/portfolio-audit-2026-07-12. --------- Co-authored-by: Sage Hart <sagehart@MacBookAir.attlocal.net>
1 parent ecc3599 commit 8ad3fe1

14 files changed

Lines changed: 839 additions & 36 deletions

.cursor/hooks.json

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
{
2+
"version": 1,
3+
"hooks": {
4+
"beforeShellExecution": [
5+
{
6+
"command": "python3 .cursor/hooks/guard-shell.py",
7+
"failClosed": false
8+
},
9+
{
10+
"command": "python3 .cursor/hooks/guard-network.py",
11+
"matcher": "curl|wget|scp|nc ",
12+
"failClosed": false
13+
}
14+
],
15+
"beforeMCPExecution": [
16+
{
17+
"command": "python3 .cursor/hooks/guard-mcp.py",
18+
"failClosed": false
19+
}
20+
],
21+
"beforeReadFile": [
22+
{
23+
"command": "python3 .cursor/hooks/protect-secrets.py",
24+
"failClosed": true
25+
}
26+
],
27+
"beforeSubmitPrompt": [
28+
{
29+
"command": "python3 .cursor/hooks/scan-prompt.py",
30+
"failClosed": true
31+
}
32+
],
33+
"stop": [
34+
{
35+
"command": "python3 .cursor/hooks/verify-on-stop.py",
36+
"loop_limit": 3
37+
}
38+
]
39+
}
40+
}

.cursor/hooks/_common.py

Lines changed: 265 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,265 @@
1+
"""Shared helpers for Cursor v2 lifecycle hooks.
2+
3+
Imported by sibling hook scripts. Robust to the working directory Cursor
4+
launches hooks from, since callers add this file's directory to sys.path.
5+
6+
Design law (v2): a guard exists only if `harness redteam` exercises it. Keep
7+
detection logic here so a single corpus can test every consumer.
8+
"""
9+
from __future__ import annotations
10+
11+
import datetime
12+
import json
13+
import os
14+
import re
15+
import sys
16+
import uuid
17+
from pathlib import Path
18+
from typing import Any
19+
20+
CURSOR_HOME = os.path.expanduser("~/.cursor")
21+
LOG_DIR = os.environ.get("CURSOR_HOOK_LOG_DIR", os.path.join(CURSOR_HOME, "logs"))
22+
MEMORY_DIR = os.path.join(CURSOR_HOME, "memory")
23+
_TRACE_ID = os.environ.get("CURSOR_TRACE_ID") or uuid.uuid4().hex
24+
25+
26+
_TRACE_ID = os.environ.get("CURSOR_TRACE_ID") or uuid.uuid4().hex
27+
_HOOK_CONTEXT: dict[str, Any] | None = None
28+
29+
30+
def resolve_workspace_root(
31+
payload: dict[str, Any] | None = None,
32+
*,
33+
cwd: str | None = None,
34+
) -> str:
35+
"""Best-effort repo root for C17 log attribution (IDE + cloud)."""
36+
if payload:
37+
for key in ("workspace_roots", "workspaceRoots", "roots"):
38+
roots = payload.get(key)
39+
if isinstance(roots, list) and roots:
40+
root = str(roots[0]).strip()
41+
if root:
42+
return root
43+
for key in ("cwd", "workingDirectory", "working_directory"):
44+
value = payload.get(key)
45+
if isinstance(value, str) and value.strip():
46+
return value.strip()
47+
for env_key in (
48+
"CURSOR_PROJECT_DIR",
49+
"CURSOR_WORKSPACE_ROOT",
50+
"CLAUDE_PROJECT_DIR",
51+
):
52+
value = os.environ.get(env_key)
53+
if value and value.strip():
54+
return value.strip()
55+
if cwd and cwd.strip():
56+
return cwd.strip()
57+
return os.getcwd()
58+
59+
60+
def _repo_profile_at(root: Path) -> dict[str, Any] | None:
61+
path = root / ".harness" / "profile.yaml"
62+
if not path.is_file():
63+
return None
64+
try:
65+
import yaml # type: ignore
66+
67+
data = yaml.safe_load(path.read_text(encoding="utf-8"))
68+
return data if isinstance(data, dict) else None
69+
except Exception: # noqa: BLE001
70+
return None
71+
72+
73+
def _repo_profile_name_for(root: str) -> str | None:
74+
try:
75+
prof = _repo_profile_at(Path(root))
76+
return prof.get("profile") if prof else None
77+
except Exception: # noqa: BLE001
78+
return None
79+
80+
81+
def read_repo_profile(workspace_root: str | None = None) -> dict[str, Any] | None:
82+
"""Best-effort read of .harness/profile.yaml from a workspace root."""
83+
root = workspace_root or resolve_workspace_root()
84+
return _repo_profile_at(Path(root))
85+
86+
87+
def log_event(
88+
event: str,
89+
data: dict[str, Any],
90+
*,
91+
context: dict[str, Any] | None = None,
92+
) -> None:
93+
"""Append a structured event to today's JSONL session log. Never raises."""
94+
try:
95+
ctx = context if context is not None else _HOOK_CONTEXT
96+
workspace_root = resolve_workspace_root(ctx)
97+
os.makedirs(LOG_DIR, exist_ok=True)
98+
day = datetime.date.today().isoformat()
99+
record = {
100+
"ts": datetime.datetime.now().isoformat(timespec="seconds"),
101+
"trace_id": _TRACE_ID,
102+
"span_id": uuid.uuid4().hex[:16],
103+
"event": event,
104+
"workspace_root": workspace_root,
105+
"profile": _repo_profile_name_for(workspace_root),
106+
**data,
107+
}
108+
if ctx and ctx.get("loop_count") is not None and "loop_count" not in record:
109+
record["loop_count"] = ctx.get("loop_count")
110+
with open(os.path.join(LOG_DIR, f"{day}.jsonl"), "a", encoding="utf-8") as fh:
111+
fh.write(json.dumps(record) + "\n")
112+
except Exception:
113+
pass # observability must never break the agent
114+
115+
116+
def read_input() -> dict[str, Any]:
117+
"""Read and parse the JSON payload Cursor sends on stdin. Never raises."""
118+
global _HOOK_CONTEXT
119+
try:
120+
raw = sys.stdin.read()
121+
payload = json.loads(raw) if raw.strip() else {}
122+
_HOOK_CONTEXT = payload if isinstance(payload, dict) else {}
123+
return _HOOK_CONTEXT
124+
except Exception:
125+
_HOOK_CONTEXT = {}
126+
return {}
127+
128+
129+
def emit(obj: dict[str, Any]) -> None:
130+
"""Write a JSON response and flush."""
131+
sys.stdout.write(json.dumps(obj))
132+
sys.stdout.flush()
133+
134+
135+
def allow() -> None:
136+
emit({"permission": "allow"})
137+
138+
139+
def deny(user_message: str, agent_message: str | None = None) -> None:
140+
out: dict[str, Any] = {"permission": "deny", "user_message": user_message}
141+
if agent_message:
142+
out["agent_message"] = agent_message
143+
emit(out)
144+
145+
146+
# --- Secret detection -------------------------------------------------------
147+
# High-confidence, low-false-positive patterns for live credentials.
148+
SECRET_PATTERNS: list[tuple[re.Pattern[str], str]] = [
149+
(re.compile(r"-----BEGIN (?:RSA |EC |OPENSSH |DSA |PGP )?PRIVATE KEY-----"), "private key"),
150+
(re.compile(r"\bAKIA[0-9A-Z]{16}\b"), "AWS access key id"),
151+
(re.compile(r"\baws_secret_access_key\s*[:=]\s*['\"]?[A-Za-z0-9/+]{40}\b"), "AWS secret access key"),
152+
(re.compile(r"\bghp_[A-Za-z0-9]{36}\b"), "GitHub personal access token"),
153+
(re.compile(r"\bgh[ousr]_[A-Za-z0-9]{36}\b"), "GitHub token"),
154+
(re.compile(r"\bxox[baprs]-[0-9A-Za-z-]{10,}"), "Slack token"),
155+
(re.compile(r"\bsk-(?:proj-)?[A-Za-z0-9_-]{20,}\b"), "OpenAI-style API key"),
156+
(re.compile(r"\bAIza[0-9A-Za-z_-]{35}\b"), "Google API key"),
157+
(re.compile(r"\bglpat-[0-9A-Za-z_-]{20,}\b"), "GitLab personal access token"),
158+
(re.compile(r"\bsk_live_[0-9A-Za-z]{24,}\b"), "Stripe live secret key"),
159+
(re.compile(r"\bsk-ant-[A-Za-z0-9_-]{20,}\b"), "Anthropic API key"),
160+
(re.compile(r"\bxox[pae]-[0-9A-Za-z-]{10,}"), "Slack token (xoxp/xoxa/xoxe)"),
161+
]
162+
163+
164+
def find_secret(text: str) -> str | None:
165+
"""Return a label for the first high-confidence secret found, else None."""
166+
if not text:
167+
return None
168+
for pattern, label in SECRET_PATTERNS:
169+
if pattern.search(text):
170+
return label
171+
return None
172+
173+
174+
# --- Sensitive-file detection ----------------------------------------------
175+
_ALLOW_SUFFIXES = (".example", ".sample", ".template", ".dist", ".pub")
176+
_SECRET_BASENAMES = {
177+
".env", ".env.local", ".env.production", ".env.prod", ".env.staging",
178+
".env.test", ".env.test.local",
179+
".env.development.local", ".env.production.local",
180+
"id_rsa", "id_dsa", "id_ecdsa", "id_ed25519",
181+
".netrc", "credentials", "credentials.json", ".npmrc", ".pypirc", ".git-credentials",
182+
}
183+
_SECRET_EXTS = {".pem", ".key", ".p12", ".pfx", ".keystore", ".jks", ".asc", ".ppk"}
184+
185+
186+
def is_sensitive_file(path: str) -> bool:
187+
if not path:
188+
return False
189+
base = os.path.basename(path)
190+
lower = base.lower()
191+
if lower.endswith(_ALLOW_SUFFIXES):
192+
return False
193+
if base in _SECRET_BASENAMES:
194+
return True
195+
if lower.startswith(".env"):
196+
return True
197+
_, ext = os.path.splitext(lower)
198+
if ext in _SECRET_EXTS:
199+
return True
200+
return False
201+
202+
203+
# --- Destructive data-layer detection (shared by shell + MCP guards) --------
204+
# Narrow, high-confidence patterns for irreversible data/disk destruction that
205+
# can arrive either as a shell command or as serialized MCP tool arguments
206+
# (e.g. a database MCP running DROP). Fail-open nets, not boundaries.
207+
DATA_DESTRUCTIVE: list[tuple[re.Pattern[str], str]] = [
208+
(re.compile(r"\b(drop\s+database|drop\s+table|truncate\s+table)\b", re.IGNORECASE),
209+
"Destructive SQL (DROP / TRUNCATE)."),
210+
(re.compile(r"\bdelete\s+from\s+\w+\s*(;|$)", re.IGNORECASE),
211+
"Unfiltered DELETE (no WHERE clause)."),
212+
(re.compile(r"\bmkfs(\.\w+)?\b", re.IGNORECASE), "Filesystem format command."),
213+
(re.compile(r"\bdd\b.*\bof=/dev/", re.IGNORECASE), "dd writing to a raw device."),
214+
]
215+
216+
217+
def find_data_destructive(text: str) -> str | None:
218+
if not text:
219+
return None
220+
for pattern, why in DATA_DESTRUCTIVE:
221+
if pattern.search(text):
222+
return why
223+
return None
224+
225+
226+
# --- Agent worktree integration guard (shared pattern for guard-shell) ----------
227+
_COMPOSE_RE = re.compile(r"\bdocker\s+compose\b|\bdocker-compose\b", re.IGNORECASE)
228+
_INTEGRATION_SCRIPT_RE = re.compile(
229+
r"(?:^|[;&|\s])(?:\./)?scripts/"
230+
r"(?:smoke-test(?:-phase[23])?|deploy-stack|seed|register-debezium-connector|"
231+
r"wait-outbox-drained|verify-state-twin-pipeline|demo-phase3|verify-worktree-merge|"
232+
r"register-schemas|submit-flink-job)"
233+
r"\.sh\b",
234+
re.IGNORECASE,
235+
)
236+
_WORKTREE_INTEGRATION_MSG = (
237+
"Integration/stack scripts from an agent worktree (.worktrees/) are blocked — "
238+
"they hit the main Docker stack and produce false confidence. "
239+
"Parent runs merge verification from the main repo root "
240+
"(./scripts/verify-worktree-merge.sh). "
241+
"Set ALLOW_WORKTREE_COMPOSE=1 only when the user explicitly overrides."
242+
)
243+
244+
245+
def _in_worktree_context(payload: dict[str, Any]) -> bool:
246+
command = (payload.get("command") or "").strip()
247+
cwd = (payload.get("cwd") or payload.get("workingDirectory") or "").strip()
248+
return ".worktrees" in f"{cwd} {command}"
249+
250+
251+
def find_worktree_integration_block(payload: dict[str, Any]) -> str | None:
252+
"""Block Compose and integration smoke/stack scripts from worktree cwd."""
253+
if os.environ.get("ALLOW_WORKTREE_COMPOSE") == "1":
254+
return None
255+
if not _in_worktree_context(payload):
256+
return None
257+
command = (payload.get("command") or "").strip()
258+
if _COMPOSE_RE.search(command) or _INTEGRATION_SCRIPT_RE.search(command):
259+
return _WORKTREE_INTEGRATION_MSG
260+
return None
261+
262+
263+
def find_worktree_compose_block(payload: dict[str, Any]) -> str | None:
264+
"""Backward-compatible alias."""
265+
return find_worktree_integration_block(payload)

0 commit comments

Comments
 (0)