Security
Security#
claude-review processes untrusted input (PR titles, descriptions, diffs, comments) and sends it to an LLM. Multiple defense layers prevent prompt injection, data exfiltration, and abuse.
Threat Model#
The primary threats are:
Prompt injection – an attacker embeds instructions in a PR title, description, or diff that override the system prompt.
Response hijacking – injected instructions cause the LLM to produce harmful output (e.g. approving a malicious PR).
Data exfiltration – injected instructions cause the LLM to leak system prompt content or secrets.
Abuse – excessive API usage or unauthorized access to the review service.
Anti-Injection Layers#
1. Content Sanitization#
All user-controlled content is sanitized before being included in the prompt:
Zero-width characters are stripped: U+200B, U+200C, U+200D, U+FEFF, U+00AD, U+034F, U+180E, U+2060-U+2064.
Bidirectional override characters are stripped: U+202A-U+202E, U+2066-U+2069, U+200E-U+200F, U+061C.
Unicode tag characters are stripped: U+E0000-U+E007F (used for language tagging, can hide content).
Variation selectors are stripped: U+FE00-U+FE0F (can alter glyph appearance).
Interlinear annotation anchors are stripped: U+FFF9-U+FFFB.
Control characters are removed (except \n, \r, \t).
Truncation respects UTF-8 char boundaries to avoid producing invalid strings.
Content truncation enforces length limits per field:
| PR title | 500 chars |
| PR description | 10,000 chars |
| Diff | 500,000 chars |
| Comments | 5,000 chars per comment |
2. Random Fence Delimiters#
User content is wrapped in randomly generated fence delimiters:
===BOUNDARY_a1b2c3d4e5f67890fedcba9876543210===
<user content here>
===BOUNDARY_a1b2c3d4e5f67890fedcba9876543210===
The boundary ID is a 128-bit (two 64-bit) random hex value generated fresh per request, providing full entropy against prediction. The system prompt instructs the LLM to treat all content between boundary delimiters as untrusted data and never follow instructions found within them.
Because the delimiter is random and unpredictable, an attacker cannot craft a payload that closes the boundary early.
3. Canary Token Verification#
Each request includes a randomly generated canary token. The system prompt instructs the LLM to echo this token in its response. After receiving the response, the application verifies the canary is present.
If the canary is missing, the response is flagged as potentially hijacked. The application logs a warning and falls back to using the raw response with additional caution.
4. Suspicious Pattern Detection#
User content is scanned against a set of built-in regex patterns that match common prompt injection phrases:
“ignore previous instructions”
“you are now”
“system prompt”
“disregard prior”
“new instructions:”
“forget everything”
“override your instructions”
“act as”
“pretend you are”
“jailbreak”
“DAN mode”
“from now on”
And others. When a match is found, the content is flagged as suspicious. The LLM still processes it, but with an explicit warning injected into the system prompt alerting it to the attempted injection.
You can add custom patterns via the config:
[anti_injection]
suspicious_patterns = [
"(?i)execute\\s+this\\s+code",
"(?i)run\\s+the\\s+following",
]
Output Safety Check#
Review output passes through two validation stages before being posted to GitHub:
Stage 1: Canary Verification#
The canary token must be present in the response.
The response is checked for leaked system prompt fragments.
The response is parsed as JSON; if parsing fails, findings are discarded and only the raw text summary is posted.
The canary token is stripped from the final output.
Stage 2: LLM Safety Audit#
A second, independent LLM call (using a fast model like Claude Haiku by default) examines the parsed review findings before they are posted. This catches issues the primary reviewer may have been tricked into producing:
Prompt injection artifacts – off-topic responses, instructions copied from the PR rather than genuine review
Harmful content – offensive language, personal attacks, discriminatory remarks
Sensitive data leakage – API keys, passwords, internal URLs, or system prompt content in findings
Hallucinated references – findings referencing files or lines not in the diff
Social engineering – attempts to manipulate the PR author into dangerous actions
Flagged findings are removed from the review, and a note is appended to the summary. The safety checker uses its own system prompt that is isolated from the primary review, preventing a single injection from compromising both stages.
[safety_check]
enabled = true
provider = "anthropic"
model = "claude-haiku-4-5-20251001"
max_tokens = 1024
# api_key falls back to the main LLM api_key
The safety checker can use a different provider or model than the primary reviewer. For example, you can review with Claude Sonnet and safety-check with OpenAI, or vice versa, to avoid correlated failures.
Webhook Signature Verification#
Every incoming webhook request is verified using HMAC-SHA256:
GitHub signs the request body with the webhook secret and sends the signature in the X-Hub-Signature-256 header.
The server computes HMAC-SHA256(secret, body) and compares it to the provided signature using a constant-time comparison.
Requests with missing or invalid signatures are rejected with 401 Unauthorized.
The webhook secret should be set via the GITHUB_WEBHOOK_SECRET environment variable.
Sandbox Isolation#
When a review requires executing tools (via MCP), the sandbox restricts what those tools can access. See Sandbox for details.
Key properties:
Filesystem access is restricted to explicitly allowed paths.
Network access can be fully disabled (none), partially restricted (limited), or unrestricted (full).
Processes are killed when the timeout expires.
On Linux, bwrap provides namespace-based isolation.
On Windows, Job Objects enforce process limits and termination-on-close.
Token Handling#
The GitHub App private key is loaded from a file on disk or from the GITHUB_APP_PRIVATE_KEY environment variable (base64-encoded).
JWTs are generated with 10-minute expiry and 60-second clock skew tolerance.
Installation tokens are cached in memory and refreshed 5 minutes before expiry.
API keys are stored using the secrecy crate, which prevents accidental logging via Debug or Display trait implementations.
Git clone operations pass credentials via GIT_CONFIG_KEY environment variables rather than embedding tokens in URLs (which would be visible in ps output and git config files).
Rate Limiting#
Per-repository token bucket rate limiting prevents abuse:
Default: 60 requests per hour with a burst of 5.
Specific users can be added to bypass_users to exempt them from limits.
When rate-limited, the server responds with 429 Too Many Requests and a Retry-After hint.
Whitelists#
Organization, user, and repository whitelists restrict which events are processed. Requests from non-whitelisted sources are silently ignored (the server returns 200 OK but performs no review).