import json
import os
import shutil
import subprocess
import sys
def deny(reason):
print(json.dumps({
"permissionDecision": "deny",
"permissionDecisionReason": reason,
}))
sys.exit(0)
def fail_action():
return "allow" if os.environ.get("TIRITH_FAIL_OPEN") == "1" else "deny"
def fail_closed(reason):
if fail_action() == "deny":
deny(reason)
else:
sys.exit(0)
def _hook_event(event, detail=None):
tirith_bin = os.environ.get("TIRITH_BIN") or shutil.which("tirith") or "tirith"
try:
cmd = [
tirith_bin,
"hook-event",
"--integration",
"copilot-cli",
"--hook-type",
"pre_tool_use",
"--event",
event,
]
if detail:
cmd.extend(["--detail", detail])
subprocess.Popen(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
except Exception:
pass
def _build_reason(stdout):
text = "Tirith security check failed"
if stdout and stdout.strip():
try:
verdict = json.loads(stdout)
findings = verdict.get("findings", [])
if findings:
parts = []
for f in findings:
title = f.get("title", f.get("rule_id", "unknown"))
severity = f.get("severity", "")
parts.append(f"[{severity}] {title}" if severity else title)
text = "Tirith: " + "; ".join(parts)
except json.JSONDecodeError:
text = stdout.strip()[:500]
return text
def _extract_command(data):
if data.get("toolName") != "bash":
return ("allow", None)
raw = data.get("toolArgs", "")
if isinstance(raw, str):
try:
args = json.loads(raw)
except json.JSONDecodeError:
return ("deny", "tirith: malformed toolArgs JSON for bash tool — blocked for safety")
elif isinstance(raw, dict):
args = raw
else:
return ("deny", "tirith: unexpected toolArgs type for bash tool — blocked for safety")
if not isinstance(args, dict):
return ("deny", "tirith: toolArgs did not decode to an object — blocked for safety")
cmd = args.get("command")
if isinstance(cmd, str) and cmd.strip():
return ("check", cmd)
return ("deny", "tirith: missing or empty 'command' in bash toolArgs — blocked for safety")
def main():
try:
raw = sys.stdin.read()
if not raw.strip():
fail_closed("tirith: empty hook input — blocked for safety")
return
data = json.loads(raw)
except (json.JSONDecodeError, OSError):
_hook_event("parse_error")
fail_closed("tirith: failed to parse hook input — blocked for safety")
return
if not isinstance(data, dict):
fail_closed("tirith: invalid hook input format — blocked for safety")
return
kind, value = _extract_command(data)
if kind == "allow":
sys.exit(0)
if kind == "deny":
_hook_event("malformed_payload")
fail_closed(value)
return
command = value
tirith_bin = os.environ.get("TIRITH_BIN") or shutil.which("tirith") or "tirith"
env = os.environ.copy()
env["TIRITH_INTEGRATION"] = "copilot-cli"
try:
result = subprocess.run(
[
tirith_bin,
"check",
"--json",
"--non-interactive",
"--shell",
"posix",
"--",
command,
],
capture_output=True,
text=True,
timeout=10,
env=env,
)
except FileNotFoundError:
fail_closed(f"tirith: {tirith_bin} not found — install tirith or set TIRITH_FAIL_OPEN=1")
return
except subprocess.TimeoutExpired:
_hook_event("timeout")
fail_closed("tirith: check timed out — blocked for safety")
return
except OSError as e:
_hook_event("unexpected_exit", str(e))
fail_closed(f"tirith: OS error running check — {e}")
return
if result.returncode not in (0, 1, 2):
_hook_event("unexpected_exit", f"exit code {result.returncode}")
fail_closed(f"tirith: unexpected exit code {result.returncode} — blocked for safety")
return
if result.returncode != 0 and not result.stdout.strip():
_hook_event("unexpected_exit", f"exit code {result.returncode} with no output")
fail_closed("tirith: check returned non-zero with no output — blocked for safety")
return
if result.returncode == 0:
_hook_event("check_ok")
sys.exit(0)
if result.returncode == 2:
warn_action = os.environ.get("TIRITH_HOOK_WARN_ACTION", "allow").lower()
if warn_action not in ("allow", "deny"):
warn_action = "allow"
if warn_action != "deny":
_hook_event("warn_allowed")
sys.exit(0)
_hook_event("warn_denied")
else:
_hook_event("check_block")
deny(_build_reason(result.stdout))
if __name__ == "__main__":
try:
main()
except Exception:
if os.environ.get("TIRITH_FAIL_OPEN") == "1":
sys.exit(0)
print(json.dumps({
"permissionDecision": "deny",
"permissionDecisionReason": "tirith: unexpected hook error — blocked for safety",
}))
sys.exit(0)