agentsec-firewall 0.1.1__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.
@@ -0,0 +1,171 @@
1
+ Metadata-Version: 2.4
2
+ Name: agentsec-firewall
3
+ Version: 0.1.1
4
+ Summary: Policy-enforced firewall for AI agent tool calls
5
+ License-Expression: MIT
6
+ Requires-Python: >=3.11
7
+ Description-Content-Type: text/markdown
8
+ Requires-Dist: agentsec-core>=0.1.0
9
+ Requires-Dist: click>=8.0
10
+ Requires-Dist: pyyaml>=6.0
11
+ Provides-Extra: dev
12
+ Requires-Dist: pytest>=8.0; extra == "dev"
13
+ Requires-Dist: pytest-cov>=4.0; extra == "dev"
14
+ Requires-Dist: ruff>=0.3.0; extra == "dev"
15
+
16
+ # agentfirewall
17
+
18
+ Policy-enforced firewall for AI agent tool calls. Intercepts, evaluates, and audits every tool invocation against a YAML security policy.
19
+
20
+ Part of the [AgentSec](https://github.com/agentsec) suite -- open-source security primitives for AI agents.
21
+
22
+ ## Install
23
+
24
+ ```bash
25
+ pip install agentsec-firewall
26
+ ```
27
+
28
+ > **Premium:** Get enterprise security policies, advanced secret detection, and webhook alerting at [zazmatt.gumroad.com/l/kjwwhn](https://zazmatt.gumroad.com/l/kjwwhn)
29
+
30
+ ## Quick Start
31
+
32
+ ```bash
33
+ # 1. Initialize policy and audit log
34
+ agentfirewall init
35
+
36
+ # 2. Install hooks into Claude Code
37
+ agentfirewall install
38
+
39
+ # 3. Done -- every tool call is now checked against .agentfirewall/policy.yaml
40
+ ```
41
+
42
+ ## How It Works
43
+
44
+ ```
45
+ Tool Call (e.g. Bash "rm -rf /")
46
+ |
47
+ v
48
+ +-----------+
49
+ | PreToolUse | <-- Claude Code hook reads stdin JSON
50
+ | Hook |
51
+ +-----+-----+
52
+ |
53
+ v
54
+ +-----------+
55
+ | Interceptor| <-- Evaluates against policy.yaml
56
+ | .check() | Scans params for secrets
57
+ +-----+-----+
58
+ |
59
+ +----+----+
60
+ | |
61
+ ALLOW DENY ---------> exit 2 (blocks tool call)
62
+ | + JSON reason to stdout
63
+ v
64
+ Tool Executes
65
+ |
66
+ v
67
+ +-----------+
68
+ | PostToolUse| <-- Logs execution to audit.jsonl
69
+ | Hook |
70
+ +-----------+
71
+ ```
72
+
73
+ ## Policy Reference
74
+
75
+ Policies are YAML files at `.agentfirewall/policy.yaml`:
76
+
77
+ ```yaml
78
+ version: "1.0"
79
+ name: "my-policy"
80
+ description: "Custom security policy"
81
+ default_action: log # allow | deny | log | alert
82
+
83
+ rules:
84
+ - name: allow-read-operations
85
+ tools: ["Read", "Glob", "Grep"] # fnmatch patterns
86
+ action: allow
87
+ reason: "Read operations are safe"
88
+
89
+ - name: block-dangerous-bash
90
+ tools: ["Bash"]
91
+ resources: ["rm -rf *", "sudo *"] # resource patterns
92
+ action: deny
93
+ reason: "Dangerous shell commands blocked"
94
+
95
+ - name: alert-mcp-tools
96
+ tools: ["mcp__*"] # wildcards supported
97
+ action: alert
98
+ reason: "MCP tool calls flagged for review"
99
+
100
+ - name: log-all-writes
101
+ tools: ["Write", "Edit"]
102
+ action: log
103
+ reason: "File modifications logged"
104
+ ```
105
+
106
+ **Actions:**
107
+ - `allow` -- permit the tool call
108
+ - `deny` -- block the tool call (exit code 2)
109
+ - `log` -- permit but log to audit trail
110
+ - `alert` -- permit, log, and fire webhook alert
111
+
112
+ ## CLI Reference
113
+
114
+ | Command | Description |
115
+ |---------|-------------|
116
+ | `agentfirewall init` | Create `.agentfirewall/` with default policy and audit log |
117
+ | `agentfirewall install` | Add PreToolUse/PostToolUse hooks to `.claude/settings.local.json` |
118
+ | `agentfirewall uninstall` | Remove agentfirewall hooks from settings |
119
+ | `agentfirewall validate` | Validate policy YAML and print rule summary |
120
+ | `agentfirewall audit` | Query audit log (supports `--tail`, `--tool`, `--action` filters) |
121
+ | `agentfirewall scan [PATH]` | Run security scanner on project files |
122
+
123
+ ## Python API
124
+
125
+ ```python
126
+ from agentfirewall import Interceptor, PolicyViolationError
127
+ from agentsec_core.schemas import PolicyAction
128
+
129
+ # From a policy file
130
+ interceptor = Interceptor(policy_path=".agentfirewall/policy.yaml")
131
+
132
+ # Check a tool call (returns PolicyDecision, never raises)
133
+ decision = interceptor.check("Bash", {"command": "rm -rf /"})
134
+ if decision.action == PolicyAction.DENY:
135
+ print(f"Blocked: {decision.reason}")
136
+
137
+ # Check and raise on deny
138
+ try:
139
+ interceptor.check_or_raise("Bash", {"command": "rm -rf /"})
140
+ except PolicyViolationError as e:
141
+ print(f"Violation: {e.decision.reason}")
142
+
143
+ # Decorator pattern
144
+ @interceptor.wrap("data_export")
145
+ def export_data(**kwargs):
146
+ ... # Only runs if policy allows
147
+ ```
148
+
149
+ ## Free vs Premium
150
+
151
+ | Feature | Free (OSS) | Premium |
152
+ |---------|:----------:|:-------:|
153
+ | YAML policy enforcement | Yes | Yes |
154
+ | PreToolUse / PostToolUse hooks | Yes | Yes |
155
+ | Audit log (JSONL) | Yes | Yes |
156
+ | Secret scanning (23 patterns) | Yes | Yes |
157
+ | Enterprise policy templates | -- | Yes |
158
+ | Webhook alerts (Slack, Discord) | -- | Yes |
159
+ | PII/GDPR filtering rules | -- | Yes |
160
+ | Priority support | -- | Yes |
161
+
162
+ [Get Premium -- $10](https://zazmatt.gumroad.com/l/kjwwhn)
163
+
164
+ ## Requirements
165
+
166
+ - Python 3.11+
167
+ - [agentsec-core](../agentsec-core/) >= 0.1.0
168
+
169
+ ## License
170
+
171
+ MIT
@@ -0,0 +1,156 @@
1
+ # agentfirewall
2
+
3
+ Policy-enforced firewall for AI agent tool calls. Intercepts, evaluates, and audits every tool invocation against a YAML security policy.
4
+
5
+ Part of the [AgentSec](https://github.com/agentsec) suite -- open-source security primitives for AI agents.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ pip install agentsec-firewall
11
+ ```
12
+
13
+ > **Premium:** Get enterprise security policies, advanced secret detection, and webhook alerting at [zazmatt.gumroad.com/l/kjwwhn](https://zazmatt.gumroad.com/l/kjwwhn)
14
+
15
+ ## Quick Start
16
+
17
+ ```bash
18
+ # 1. Initialize policy and audit log
19
+ agentfirewall init
20
+
21
+ # 2. Install hooks into Claude Code
22
+ agentfirewall install
23
+
24
+ # 3. Done -- every tool call is now checked against .agentfirewall/policy.yaml
25
+ ```
26
+
27
+ ## How It Works
28
+
29
+ ```
30
+ Tool Call (e.g. Bash "rm -rf /")
31
+ |
32
+ v
33
+ +-----------+
34
+ | PreToolUse | <-- Claude Code hook reads stdin JSON
35
+ | Hook |
36
+ +-----+-----+
37
+ |
38
+ v
39
+ +-----------+
40
+ | Interceptor| <-- Evaluates against policy.yaml
41
+ | .check() | Scans params for secrets
42
+ +-----+-----+
43
+ |
44
+ +----+----+
45
+ | |
46
+ ALLOW DENY ---------> exit 2 (blocks tool call)
47
+ | + JSON reason to stdout
48
+ v
49
+ Tool Executes
50
+ |
51
+ v
52
+ +-----------+
53
+ | PostToolUse| <-- Logs execution to audit.jsonl
54
+ | Hook |
55
+ +-----------+
56
+ ```
57
+
58
+ ## Policy Reference
59
+
60
+ Policies are YAML files at `.agentfirewall/policy.yaml`:
61
+
62
+ ```yaml
63
+ version: "1.0"
64
+ name: "my-policy"
65
+ description: "Custom security policy"
66
+ default_action: log # allow | deny | log | alert
67
+
68
+ rules:
69
+ - name: allow-read-operations
70
+ tools: ["Read", "Glob", "Grep"] # fnmatch patterns
71
+ action: allow
72
+ reason: "Read operations are safe"
73
+
74
+ - name: block-dangerous-bash
75
+ tools: ["Bash"]
76
+ resources: ["rm -rf *", "sudo *"] # resource patterns
77
+ action: deny
78
+ reason: "Dangerous shell commands blocked"
79
+
80
+ - name: alert-mcp-tools
81
+ tools: ["mcp__*"] # wildcards supported
82
+ action: alert
83
+ reason: "MCP tool calls flagged for review"
84
+
85
+ - name: log-all-writes
86
+ tools: ["Write", "Edit"]
87
+ action: log
88
+ reason: "File modifications logged"
89
+ ```
90
+
91
+ **Actions:**
92
+ - `allow` -- permit the tool call
93
+ - `deny` -- block the tool call (exit code 2)
94
+ - `log` -- permit but log to audit trail
95
+ - `alert` -- permit, log, and fire webhook alert
96
+
97
+ ## CLI Reference
98
+
99
+ | Command | Description |
100
+ |---------|-------------|
101
+ | `agentfirewall init` | Create `.agentfirewall/` with default policy and audit log |
102
+ | `agentfirewall install` | Add PreToolUse/PostToolUse hooks to `.claude/settings.local.json` |
103
+ | `agentfirewall uninstall` | Remove agentfirewall hooks from settings |
104
+ | `agentfirewall validate` | Validate policy YAML and print rule summary |
105
+ | `agentfirewall audit` | Query audit log (supports `--tail`, `--tool`, `--action` filters) |
106
+ | `agentfirewall scan [PATH]` | Run security scanner on project files |
107
+
108
+ ## Python API
109
+
110
+ ```python
111
+ from agentfirewall import Interceptor, PolicyViolationError
112
+ from agentsec_core.schemas import PolicyAction
113
+
114
+ # From a policy file
115
+ interceptor = Interceptor(policy_path=".agentfirewall/policy.yaml")
116
+
117
+ # Check a tool call (returns PolicyDecision, never raises)
118
+ decision = interceptor.check("Bash", {"command": "rm -rf /"})
119
+ if decision.action == PolicyAction.DENY:
120
+ print(f"Blocked: {decision.reason}")
121
+
122
+ # Check and raise on deny
123
+ try:
124
+ interceptor.check_or_raise("Bash", {"command": "rm -rf /"})
125
+ except PolicyViolationError as e:
126
+ print(f"Violation: {e.decision.reason}")
127
+
128
+ # Decorator pattern
129
+ @interceptor.wrap("data_export")
130
+ def export_data(**kwargs):
131
+ ... # Only runs if policy allows
132
+ ```
133
+
134
+ ## Free vs Premium
135
+
136
+ | Feature | Free (OSS) | Premium |
137
+ |---------|:----------:|:-------:|
138
+ | YAML policy enforcement | Yes | Yes |
139
+ | PreToolUse / PostToolUse hooks | Yes | Yes |
140
+ | Audit log (JSONL) | Yes | Yes |
141
+ | Secret scanning (23 patterns) | Yes | Yes |
142
+ | Enterprise policy templates | -- | Yes |
143
+ | Webhook alerts (Slack, Discord) | -- | Yes |
144
+ | PII/GDPR filtering rules | -- | Yes |
145
+ | Priority support | -- | Yes |
146
+
147
+ [Get Premium -- $10](https://zazmatt.gumroad.com/l/kjwwhn)
148
+
149
+ ## Requirements
150
+
151
+ - Python 3.11+
152
+ - [agentsec-core](../agentsec-core/) >= 0.1.0
153
+
154
+ ## License
155
+
156
+ MIT
@@ -0,0 +1,5 @@
1
+ """agentfirewall: Policy-enforced firewall for AI agent tool calls."""
2
+
3
+ __version__ = "0.1.0"
4
+
5
+ from .interceptor import Interceptor, PolicyViolationError
@@ -0,0 +1,5 @@
1
+ """Allow running as python3 -m agentfirewall."""
2
+
3
+ from .cli import main
4
+
5
+ main()
@@ -0,0 +1,236 @@
1
+ """Click-based CLI for agentfirewall."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import logging
7
+ from pathlib import Path
8
+
9
+ import click
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+ DEFAULT_POLICY_DIR = ".agentfirewall"
14
+ DEFAULT_POLICY_PATH = f"{DEFAULT_POLICY_DIR}/policy.yaml"
15
+ DEFAULT_LOG_PATH = f"{DEFAULT_POLICY_DIR}/audit.jsonl"
16
+
17
+ DEFAULT_POLICY_YAML = """\
18
+ version: "1.0"
19
+ name: "default-agent-policy"
20
+ description: "Default security policy -- blocks dangerous operations, logs everything"
21
+ default_action: log
22
+ rules:
23
+ - name: allow-read-operations
24
+ tools: ["Read", "Glob", "Grep"]
25
+ action: allow
26
+ reason: "Read operations are safe"
27
+ - name: block-dangerous-bash
28
+ tools: ["Bash"]
29
+ resources: ["rm -rf *", "sudo *", "chmod 777 *", "curl * | sh", "wget * | sh"]
30
+ action: deny
31
+ reason: "Dangerous shell commands blocked"
32
+ - name: alert-mcp-tools
33
+ tools: ["mcp__*"]
34
+ action: alert
35
+ reason: "MCP tool calls flagged for review"
36
+ - name: log-all-writes
37
+ tools: ["Write", "Edit"]
38
+ action: log
39
+ reason: "File modifications logged"
40
+ """
41
+
42
+ HOOK_PRETOOL = {
43
+ "type": "command",
44
+ "event": "PreToolUse",
45
+ "command": "python3 -c 'from agentfirewall.hooks import pretool_hook; pretool_hook()'",
46
+ }
47
+ HOOK_POSTTOOL = {
48
+ "type": "command",
49
+ "event": "PostToolUse",
50
+ "command": "python3 -c 'from agentfirewall.hooks import posttool_hook; posttool_hook()'",
51
+ }
52
+
53
+ SETTINGS_PATH = ".claude/settings.local.json"
54
+
55
+
56
+ @click.group()
57
+ @click.version_option(package_name="agentfirewall")
58
+ def main() -> None:
59
+ """agentfirewall -- Policy-enforced firewall for AI agent tool calls."""
60
+
61
+
62
+ @main.command()
63
+ def init() -> None:
64
+ """Initialize .agentfirewall/ with default policy and audit log."""
65
+ policy_dir = Path(DEFAULT_POLICY_DIR)
66
+ policy_dir.mkdir(parents=True, exist_ok=True)
67
+
68
+ policy_file = Path(DEFAULT_POLICY_PATH)
69
+ if policy_file.exists():
70
+ click.echo(f"Policy already exists: {policy_file}")
71
+ else:
72
+ policy_file.write_text(DEFAULT_POLICY_YAML, encoding="utf-8")
73
+ click.echo(f"Created default policy: {policy_file}")
74
+
75
+ log_file = Path(DEFAULT_LOG_PATH)
76
+ if not log_file.exists():
77
+ log_file.touch()
78
+ click.echo(f"Created audit log: {log_file}")
79
+
80
+ click.echo("Initialization complete. Run `agentfirewall install` to add hooks.")
81
+
82
+
83
+ @main.command()
84
+ @click.option("--policy", default=DEFAULT_POLICY_PATH, help="Path to policy YAML file.")
85
+ def validate(policy: str) -> None:
86
+ """Validate a policy YAML file."""
87
+ from agentsec_core.policy import load_policy
88
+
89
+ try:
90
+ loaded = load_policy(policy)
91
+ except FileNotFoundError:
92
+ click.echo(f"Error: Policy file not found: {policy}", err=True)
93
+ raise SystemExit(1)
94
+ except ValueError as exc:
95
+ click.echo(f"Error: Invalid policy: {exc}", err=True)
96
+ raise SystemExit(1)
97
+
98
+ click.echo(f"Policy: {loaded.name} (v{loaded.version})")
99
+ click.echo(f"Description: {loaded.description}")
100
+ click.echo(f"Default action: {loaded.default_action.value}")
101
+ click.echo(f"Rules: {len(loaded.rules)}")
102
+ for rule in loaded.rules:
103
+ tools = ", ".join(rule.tools) if rule.tools else "*"
104
+ click.echo(f" - {rule.name}: {rule.action.value} [{tools}]")
105
+ click.echo("Policy is valid.")
106
+
107
+
108
+ @main.command()
109
+ @click.option("--tail", "tail_n", default=20, type=int, help="Number of recent events.")
110
+ @click.option("--tool", "tool_name", default=None, help="Filter by tool name.")
111
+ @click.option("--action", "action_filter", default=None, help="Filter by action (allow/deny/log/alert).")
112
+ def audit(tail_n: int, tool_name: str | None, action_filter: str | None) -> None:
113
+ """Query the audit log."""
114
+ from agentsec_core.logger import AuditLogger
115
+ from agentsec_core.schemas import PolicyAction
116
+
117
+ log_path = Path(DEFAULT_LOG_PATH)
118
+ if not log_path.exists():
119
+ click.echo("No audit log found. Run `agentfirewall init` first.", err=True)
120
+ raise SystemExit(1)
121
+
122
+ audit_logger = AuditLogger(log_path)
123
+
124
+ action_enum = None
125
+ if action_filter:
126
+ try:
127
+ action_enum = PolicyAction(action_filter.lower())
128
+ except ValueError:
129
+ click.echo(f"Error: Invalid action '{action_filter}'. Use: allow/deny/log/alert", err=True)
130
+ raise SystemExit(1)
131
+
132
+ events = audit_logger.query(tool_name=tool_name, action=action_enum)
133
+ recent = events[-tail_n:]
134
+
135
+ if not recent:
136
+ click.echo("No matching events found.")
137
+ return
138
+
139
+ click.echo(f"Showing {len(recent)} of {len(events)} events:")
140
+ for event in recent:
141
+ ts = event.timestamp.strftime("%Y-%m-%d %H:%M:%S") if event.timestamp else "N/A"
142
+ violations = f" [{len(event.violations)} violations]" if event.violations else ""
143
+ click.echo(f" {ts} | {event.action_taken.value:5s} | {event.tool_name}{violations}")
144
+
145
+
146
+ @main.command()
147
+ @click.argument("path", default=".")
148
+ def scan(path: str) -> None:
149
+ """Run security scanner on project files."""
150
+ from agentsec_core.scanner import scan_file
151
+
152
+ target = Path(path)
153
+ if not target.exists():
154
+ click.echo(f"Error: Path not found: {path}", err=True)
155
+ raise SystemExit(1)
156
+
157
+ files = [target] if target.is_file() else sorted(target.rglob("*"))
158
+ total_violations = 0
159
+
160
+ for filepath in files:
161
+ if not filepath.is_file():
162
+ continue
163
+ violations = scan_file(str(filepath))
164
+ for v in violations:
165
+ total_violations += 1
166
+ click.echo(f" [{v.severity}] {v.source}:{v.line_number} - {v.message}")
167
+
168
+ if total_violations == 0:
169
+ click.echo("No security issues found.")
170
+ else:
171
+ click.echo(f"\nFound {total_violations} issue(s).")
172
+ raise SystemExit(1)
173
+
174
+
175
+ @main.command()
176
+ def install() -> None:
177
+ """Install agentfirewall hooks into Claude Code settings."""
178
+ settings_path = Path(SETTINGS_PATH)
179
+ settings_path.parent.mkdir(parents=True, exist_ok=True)
180
+
181
+ if settings_path.exists():
182
+ settings = json.loads(settings_path.read_text(encoding="utf-8"))
183
+ else:
184
+ settings = {}
185
+
186
+ hooks = settings.get("hooks", [])
187
+
188
+ # Check for existing agentfirewall hooks
189
+ existing_commands = {h.get("command", "") for h in hooks}
190
+ added = 0
191
+
192
+ if HOOK_PRETOOL["command"] not in existing_commands:
193
+ hooks.append(HOOK_PRETOOL)
194
+ added += 1
195
+
196
+ if HOOK_POSTTOOL["command"] not in existing_commands:
197
+ hooks.append(HOOK_POSTTOOL)
198
+ added += 1
199
+
200
+ settings["hooks"] = hooks
201
+ settings_path.write_text(
202
+ json.dumps(settings, indent=2) + "\n", encoding="utf-8"
203
+ )
204
+
205
+ if added > 0:
206
+ click.echo(f"Installed {added} hook(s) into {settings_path}")
207
+ else:
208
+ click.echo("Hooks already installed.")
209
+
210
+
211
+ @main.command()
212
+ def uninstall() -> None:
213
+ """Remove agentfirewall hooks from Claude Code settings."""
214
+ settings_path = Path(SETTINGS_PATH)
215
+
216
+ if not settings_path.exists():
217
+ click.echo("No settings file found. Nothing to uninstall.")
218
+ return
219
+
220
+ settings = json.loads(settings_path.read_text(encoding="utf-8"))
221
+ hooks = settings.get("hooks", [])
222
+
223
+ firewall_commands = {HOOK_PRETOOL["command"], HOOK_POSTTOOL["command"]}
224
+ original_count = len(hooks)
225
+ hooks = [h for h in hooks if h.get("command", "") not in firewall_commands]
226
+
227
+ removed = original_count - len(hooks)
228
+ settings["hooks"] = hooks
229
+ settings_path.write_text(
230
+ json.dumps(settings, indent=2) + "\n", encoding="utf-8"
231
+ )
232
+
233
+ if removed > 0:
234
+ click.echo(f"Removed {removed} hook(s) from {settings_path}")
235
+ else:
236
+ click.echo("No agentfirewall hooks found to remove.")
@@ -0,0 +1,106 @@
1
+ """Claude Code hook integration for agentfirewall.
2
+
3
+ PreToolUse hook: reads tool call from stdin, checks policy, exits 2 to block.
4
+ PostToolUse hook: reads tool result from stdin, logs to audit trail.
5
+
6
+ Install via: agentfirewall install
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ import logging
13
+ import sys
14
+ from pathlib import Path
15
+
16
+ from agentsec_core.schemas import AuditEvent, PolicyAction
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+ # Default paths (created by `agentfirewall init`)
21
+ DEFAULT_POLICY_PATH = ".agentfirewall/policy.yaml"
22
+ DEFAULT_LOG_PATH = ".agentfirewall/audit.jsonl"
23
+
24
+
25
+ def _read_hook_input() -> dict:
26
+ """Read JSON hook input from stdin. Returns empty dict on failure."""
27
+ try:
28
+ raw = sys.stdin.read()
29
+ return json.loads(raw) if raw.strip() else {}
30
+ except (json.JSONDecodeError, OSError):
31
+ return {}
32
+
33
+
34
+ def _parse_json_field(data: dict, field: str) -> dict:
35
+ """Parse a possibly-stringified JSON field."""
36
+ value = data.get(field, "{}")
37
+ if isinstance(value, dict):
38
+ return value
39
+ try:
40
+ return json.loads(value)
41
+ except (json.JSONDecodeError, TypeError):
42
+ return {}
43
+
44
+
45
+ def pretool_hook() -> None:
46
+ """PreToolUse hook entry point.
47
+
48
+ Reads stdin JSON with tool_name and tool_input.
49
+ Checks against policy. Exits 2 to block, 0 to allow.
50
+ Outputs JSON reason on block.
51
+ """
52
+ if not Path(DEFAULT_POLICY_PATH).exists():
53
+ sys.exit(0) # No policy = allow all
54
+
55
+ from .interceptor import Interceptor
56
+
57
+ hook_data = _read_hook_input()
58
+ tool_name = hook_data.get("tool_name", "")
59
+ tool_input = _parse_json_field(hook_data, "tool_input")
60
+
61
+ try:
62
+ interceptor = Interceptor(
63
+ policy_path=DEFAULT_POLICY_PATH,
64
+ log_path=DEFAULT_LOG_PATH,
65
+ )
66
+ decision = interceptor.check(tool_name, tool_input)
67
+ except Exception as exc:
68
+ # Fail-open: don't break Claude Code if firewall crashes
69
+ sys.stderr.write(f"agentfirewall: error checking policy: {exc}\n")
70
+ sys.exit(0)
71
+
72
+ if decision.action == PolicyAction.DENY:
73
+ reason = decision.reason or "Blocked by agentfirewall policy"
74
+ output = json.dumps({"decision": "block", "reason": reason})
75
+ print(output) # noqa: T201 — CLI output
76
+ sys.exit(2)
77
+
78
+ sys.exit(0)
79
+
80
+
81
+ def posttool_hook() -> None:
82
+ """PostToolUse hook entry point.
83
+
84
+ Reads stdin JSON with tool result. Logs to audit trail.
85
+ Always exits 0 (never blocks post-execution).
86
+ """
87
+ if not Path(DEFAULT_LOG_PATH).parent.exists():
88
+ sys.exit(0)
89
+
90
+ from agentsec_core.logger import AuditLogger
91
+
92
+ hook_data = _read_hook_input()
93
+ tool_name = hook_data.get("tool_name", "")
94
+
95
+ try:
96
+ audit_logger = AuditLogger(DEFAULT_LOG_PATH)
97
+ event = AuditEvent(
98
+ event_type="tool_execution",
99
+ tool_name=tool_name,
100
+ action_taken=PolicyAction.LOG,
101
+ )
102
+ audit_logger.log(event)
103
+ except Exception as exc:
104
+ sys.stderr.write(f"agentfirewall: error logging: {exc}\n")
105
+
106
+ sys.exit(0)