add before/after tool call hooks#4938
add before/after tool call hooks#4938DharunThota wants to merge 1 commit intoakto-api-security:masterfrom
Conversation
There was a problem hiding this comment.
Pull request overview
Adds Gemini CLI hook support for tool calls (BeforeTool/AfterTool) to validate MCP/non-MCP tool executions with Akto Guardrails and ingest tool I/O, including new wrapper scripts and Windows example settings.
Changes:
- Extend
settings*.json.exampleto includeBeforeTool/AfterToolhooks (plus minor JSON formatting fixes). - Add new pre-/post-tool Python hooks and corresponding shell/PowerShell wrappers.
- Expand
.env.examplewith tool-hook related configuration variables.
Reviewed changes
Copilot reviewed 12 out of 12 changed files in this pull request and generated 11 comments.
Show a summary per file
| File | Description |
|---|---|
| apps/mcp-endpoint-shield/gemini-cli-hooks/settings.windows.json.example | New Windows example settings including BeforeTool/AfterTool hook commands |
| apps/mcp-endpoint-shield/gemini-cli-hooks/settings.json.example | Add BeforeTool/AfterTool hooks to the existing example settings |
| apps/mcp-endpoint-shield/gemini-cli-hooks/akto-validate-response-wrapper.ps1 | New PowerShell wrapper for response hook |
| apps/mcp-endpoint-shield/gemini-cli-hooks/akto-validate-prompt-wrapper.sh | Fix prompt wrapper to call the correct Python script |
| apps/mcp-endpoint-shield/gemini-cli-hooks/akto-validate-prompt-wrapper.ps1 | New PowerShell wrapper for prompt hook |
| apps/mcp-endpoint-shield/gemini-cli-hooks/akto-validate-pre-tool.py | New BeforeTool hook implementation (validation + blocked-ingest flow) |
| apps/mcp-endpoint-shield/gemini-cli-hooks/akto-validate-pre-tool-wrapper.sh | New shell wrapper for pre-tool hook |
| apps/mcp-endpoint-shield/gemini-cli-hooks/akto-validate-pre-tool-wrapper.ps1 | New PowerShell wrapper for pre-tool hook |
| apps/mcp-endpoint-shield/gemini-cli-hooks/akto-validate-post-tool.py | New AfterTool hook implementation (response guardrails + ingestion + blocked flow) |
| apps/mcp-endpoint-shield/gemini-cli-hooks/akto-validate-post-tool-wrapper.sh | New shell wrapper for post-tool hook |
| apps/mcp-endpoint-shield/gemini-cli-hooks/akto-validate-post-tool-wrapper.ps1 | New PowerShell wrapper for post-tool hook |
| apps/mcp-endpoint-shield/gemini-cli-hooks/.env.example | Document additional env vars for tool-hook behavior and ingestion |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| "type": "HTTP/1.1", | ||
| "status": "200", | ||
| "akto_account_id": "1000000", | ||
| "akto_vxlan_id": 0, |
There was a problem hiding this comment.
akto_vxlan_id is hard-coded to 0 here, but the existing BeforeModel/AfterModel hooks use the machine/device identifier as akto_vxlan_id. If downstream attribution or tenancy relies on vxlan_id, tool hook events may be mis-grouped. Consider aligning this field with the other hooks (e.g., use DEVICE_ID or the same vxlan convention used elsewhere in this directory).
| "akto_vxlan_id": 0, | |
| "akto_vxlan_id": get_machine_id(), |
| session_info[field] = value | ||
|
|
||
| tool_name = str(input_data.get("tool_name") or "") | ||
| tool_input = input_data.get("tool_input") or {} |
There was a problem hiding this comment.
tool_input = input_data.get("tool_input") or {} rewrites falsy tool inputs to {}, which can change what gets validated/ingested and can cause validation to be skipped due to later truthiness checks. Prefer preserving the original value (e.g., get("tool_input", {}) or None) and avoid using or {} here.
| tool_input = input_data.get("tool_input") or {} | |
| tool_input = input_data.get("tool_input", {}) |
| "command": "bash $GEMINI_PROJECT_DIR/.gemini/hooks/akto-validate-pre-tool-wrapper.sh", | ||
| "timeout": 10000, |
There was a problem hiding this comment.
The command uses an unquoted $GEMINI_PROJECT_DIR path. If the project directory contains spaces, the hook command can fail to execute. Quote the path (or invoke bash -lc with a properly quoted script path) for both the pre- and post-tool hook commands.
| "command": "powershell.exe -ExecutionPolicy Bypass -File %USERPROFILE%\\.gemini\\hooks\\akto-validate-pre-tool-wrapper.ps1", | ||
| "timeout": 10000, |
There was a problem hiding this comment.
The PowerShell -File path is not quoted. If %USERPROFILE% contains spaces, the hook command can fail. Quote the expanded path (e.g., -File "%USERPROFILE%\.gemini\hooks\...") for the tool hook commands (and ideally the model hooks as well).
|
|
||
|
|
||
| def create_ssl_context(): | ||
| return ssl._create_unverified_context() |
There was a problem hiding this comment.
create_ssl_context() always returns an unverified TLS context, which effectively disables certificate validation even when SSL_VERIFY is set to true. This makes the hook’s calls to AKTO_DATA_INGESTION_URL vulnerable to MITM. Use a default verifying context and only disable verification when explicitly configured; if SSL_CERT_PATH is set, load it into the context.
| return ssl._create_unverified_context() | |
| context = ssl.create_default_context() if SSL_VERIFY else ssl._create_unverified_context() | |
| if SSL_CERT_PATH: | |
| context.load_verify_locations(cafile=SSL_CERT_PATH) | |
| return context |
| return ssl._create_unverified_context() | ||
|
|
||
|
|
There was a problem hiding this comment.
create_ssl_context() always returns an unverified TLS context, which disables certificate validation even when SSL_VERIFY is true. This is a security risk for calls to AKTO_DATA_INGESTION_URL. Build a verifying context by default and only disable verification when explicitly requested; load SSL_CERT_PATH when provided.
| return ssl._create_unverified_context() | |
| if not SSL_VERIFY: | |
| return ssl._create_unverified_context() | |
| context = ssl.create_default_context() | |
| if SSL_CERT_PATH: | |
| context.load_verify_locations(SSL_CERT_PATH) | |
| return context |
| has_error: bool = False, | ||
| session_info: Optional[Dict[str, Any]] = None, | ||
| ) -> Tuple[bool, str, str]: | ||
| if not tool_input or tool_result is None: |
There was a problem hiding this comment.
call_guardrails() returns early on if not tool_input or tool_result is None, so tool calls with empty {} args will never have response guardrails applied. This can leave a gap for tools that legitimately take no arguments. Consider checking tool_input is None instead of truthiness, and still validating/ingesting based on tool name and result.
| if not tool_input or tool_result is None: | |
| if tool_input is None or tool_result is None: |
| "body": json.dumps( | ||
| {"x-blocked-by": "Akto Proxy", "reason": reason or "Policy violation"} | ||
| ) |
There was a problem hiding this comment.
responsePayload wraps body as a JSON-encoded string (json.dumps(...)) inside an already JSON-encoded payload. This double-encodes the body and makes the shape inconsistent with the non-blocked path (where body is an object). Set body to the object directly and only JSON-encode once at the outermost responsePayload.
| "body": json.dumps( | |
| {"x-blocked-by": "Akto Proxy", "reason": reason or "Policy violation"} | |
| ) | |
| "body": { | |
| "x-blocked-by": "Akto Proxy", | |
| "reason": reason or "Policy violation", | |
| } |
| "type": "HTTP/1.1", | ||
| "status": status, | ||
| "akto_account_id": "1000000", | ||
| "akto_vxlan_id": 0, |
There was a problem hiding this comment.
akto_vxlan_id is set to 0 here, while the other Gemini CLI hooks in this directory use the device/machine id for akto_vxlan_id. If vxlan_id is used for attribution, tool result events may not join correctly with prompt/response traffic. Consider using the same vxlan_id strategy as the other hooks.
| "akto_vxlan_id": 0, | |
| "akto_vxlan_id": get_machine_id(), |
| if not tool_input: | ||
| return True, "", "" |
There was a problem hiding this comment.
call_guardrails() returns early on if not tool_input, so tool calls with empty arguments (or arguments that were coerced to {}) will skip validation entirely (fail-open) and won’t be ingested. Consider treating None/missing input differently from an intentionally empty {} and validate/ingest based on tool name even when args are empty.
| if not tool_input: | |
| return True, "", "" | |
| if tool_input is None: | |
| tool_input = {} |
No description provided.