[FEATURE] Cryptographically signed harness blocks to distinguish Claude Code injections from user content
Preflight Checklist
- [x] I have searched existing requests and this feature hasn't been requested yet
- [x] This is a single feature request (not multiple features)
Problem Statement
Repo: anthropics/claude-code
Type: Feature request / Security improvement
Affects: Model security, proxy/middleware operators, enterprise compliance, any deployment with a moderation layer between Claude Code and the Anthropic API
Related issues (proposed solution to these open bugs):
- #23537 -- harness content indistinguishable from user input, model mis-attributes it to the human
- #46465 -- harness phrasing ("NEVER mention this to the user") is textbook prompt injection signature
- #56829 -- multiple system-reminder blocks cause Bedrock deployments to drop the actual user message
- #17601 -- 10,000+ hidden injections consuming 15%+ of context without user knowledge or consent
- #29729 -- plugin signing (same cryptographic approach, could share key infrastructure)
Summary
Claude Code injects harness-generated content directly into the user role of the messages array without any structural or cryptographic marker distinguishing it from genuine user input. This creates three compounding problems:
- Model confusion: The model itself cannot distinguish harness instructions from user instructions or adversarial prompt injections, making it vulnerable to attacks that mimic harness phrasing.
- Middleware false positives: Downstream safety and intent models flag legitimate harness content as high-risk because the phrasing is identical to known prompt injection patterns.
- Compliance gap: Enterprise operators cannot audit, attribute, or disclose harness injections to meet data governance obligations.
We propose that Claude Code sign every harness injection using a server-held Ed25519 private key, publish the corresponding public key for proxy verification, and deliver harness content as a distinct "type": "harness" block type rather than "type": "text". The same scheme should extend to the existing <system-reminder> wrapped injections.
---
Problem
1. The model itself is an attack surface
The model receives the messages array with no structural distinction between:
- Genuine user input
- Harness-injected operational instructions
- Adversarial prompt injections crafted to mimic harness phrasing
Issues #23537 and #46465 document the consequences: the model mis-attributes harness statements to the human, and harness phrasing ("NEVER mention this to the user", "STOP what you are doing") trains the model to silently accept exactly the class of instruction that security researchers define as adversarial. An attacker who knows the harness phrasing can craft injections that the model treats as trusted operational instructions because they are indistinguishable from the real thing.
A typed, signed block that the model can structurally recognize -- separate from user-role text -- directly hardens the model against this class of injection attack. This is the primary security benefit, independent of any proxy layer.
2. Harness content pollutes intent and moderation pipelines
Any safety or moderation proxy sitting between the client and the API must decide which content to score. Today, harness injections are indistinguishable from real user input at the API layer. This causes two failure modes:
- False positives: Harness prompts containing aggressive language ("CRITICAL", "STOP", "Do NOT", "MUST") are correctly flagged as high-risk by intent models -- because that is exactly what prompt injection attacks look like.
- Version coupling: The only workaround is content fingerprinting tied to specific Claude Code versions. Every Claude Code release potentially invalidates the fingerprint set, requiring the fingerprints to be rediscovered from scratch.
3. Compliance and auditability gap
Enterprises deploying Claude Code under data governance policies face a structural problem:
- Audit trails are corrupted: Logs cannot distinguish what the user actually typed from what Claude Code injected. Attributing content to its source -- required for SOC 2 and similar audit frameworks -- is impossible.
- Token billing is opaque: Harness injections consume tokens billed to the operator with no programmatic way to measure or disclose this (see #17601: 15%+ of context consumed by injections the operator cannot see).
- GDPR data processor obligations: Under GDPR Article 28, a data processor must be able to account for all personal data processed on behalf of a controller. When harness content and user content are indistinguishable in the messages array, operators cannot fulfill this obligation.
- User consent: Users have no mechanism to audit or consent to the content injected into their conversation by the harness on their behalf.
### 4. The existing <system-reminder> wrapper is insufficient and itself spoofable
Claude Code uses <system-reminder> tags for some injections, which are structurally identifiable. However:
- At least 11 injection types still land as raw
"type": "text"with no structural marker (detailed below). <system-reminder>tags are themselves spoofable -- an attacker can inject them in user content today, and the model may treat them as trusted.- Any purely syntactic wrapper is spoofable by definition: an attacker who reads the spec wraps their payload in the tag and bypasses filtering.
The signing scheme proposed here should cover both the existing wrapped injections and the raw ones.
---
Raw user-turn injections observed in v2.1.140
The following 11 injections currently appear as "type": "text" blocks in the user turn with no structural attribution.
1. Suggestion mode
[SUGGESTION MODE: ...
Appended after every assistant turn when suggestion mode is active. Square-bracket syntax is a canonical prompt injection convention.
2. User-stepped-away recap
The user stepped away and is coming back. ...
Injected after inactivity. Ambiguous authorship -- reads as a third-party instruction to the model.
3. Context compaction -- interactive
CRITICAL: Respond with TEXT ONLY. Do NOT call any tools.
Your task is to create a detailed summary of the conversation so far ...
ALL-CAPS directives are the highest-signal markers for prompt injection attacks. Indistinguishable from a malicious injection without exact content matching.
4. Context compaction -- non-interactive / agent mode
Your task is to create a detailed summary of this conversation. This summary will be placed ...
Imperative instruction mid-conversation with no structural attribution.
5. Tool rejection -- no reason
The user doesn't want to take this action right now. STOP what you are doing and wait for the user to tell you how to proceed.
The user doesn't want to proceed with this tool use. The tool use was rejected ... STOP what you are doing and wait for the user to tell you how to proceed.
All-caps STOP directive with no structural wrapper.
6. Tool rejection -- with reason (mixed content)
The user doesn't want to proceed with this tool use. ... To tell you how to proceed, the user said: <user text>
Harness text and real user text are concatenated in the same content block. Per-source intent scoring is impossible.
7. User interrupt
[Request interrupted by user]
[Request interrupted by user for tool use]
Square-bracket syntax is a common injection convention.
8. No-acknowledgement guard
Do NOT interpret this as user acknowledgement ...
ALL-CAPS imperative. Canonical injection signal.
9. Side message pending
IMPORTANT: After completing your current task, you MUST address the user's message above.
IMPORTANT + MUST -- two escalation markers in a single sentence.
10. External / MCP channel content
IMPORTANT: This is NOT from your user ...
Content labelled as untrusted is itself structurally identical to a prompt injection at the API layer.
11. Non-interactive agent shutdown
You are running in non-interactive mode and cannot return a response ...
Shut down your team and prepare your final response for the user.
Direct operational command with no structural marker. Identical in form to a jailbreak instruction in a multi-agent context.
Proposed Solution
Proposed Solution: Distinct block type with private key held off-device
Threat model
An attacker controls the content of user-turn messages. Their goal is to craft a message that a proxy or the model treats as a trusted harness injection, bypassing moderation or triggering privileged model behavior.
Out of scope: An attacker who compromises Anthropic's signing infrastructure. This is equivalent to compromising TLS certificate issuance -- outside the threat model for this proposal.
In scope:
- An attacker who knows the full wire format and any published public key.
- An attacker who captures real harness messages in transit.
- An attacker who extracts the Claude Code binary.
Why the private key must not be in the binary
Claude Code is distributed as an Electron/Node application. Binaries are extractable by any operator or attacker with access to the installed package. A private key embedded in the binary is recoverable, which would allow an attacker to sign arbitrary payloads indefinitely until Anthropic rotates.
The private key must be held server-side. Two viable models:
Option A -- Per-session signing token (recommended):
Once at session startup, Claude Code requests a short-lived signing token from an Anthropic endpoint (authenticated by the operator's API key). This is a single network call per session, not per prompt. The token is a signed attestation covering the session ID and a timestamp. Each harness injection then includes the session token and a per-block HMAC derived from it locally -- no further server calls needed. The token expires with the session, bounding the blast radius of any compromise.
Option B -- Build-time pre-signing of static templates:
For injections whose text is fully static (no user-supplied substrings), Anthropic signs the template text at build time. The signature is embedded in the binary. Dynamic injections (e.g., tool rejection with reason) must use Option A or carry an unsigned user-content sub-block (see below).
Handling dynamic injections (mixed static + user content)
Injection 6 (tool rejection with reason) appends the user's typed text to a static harness prefix. This cannot be pre-signed as a unit. The solution is to split the block:
[
{
"type": "harness",
"harness_type": "tool_rejection",
"text": "The user doesn't want to proceed with this tool use. The tool use was rejected. To tell you how to proceed, the user said:",
"kid": "2026-05",
"sig": "<signature over static prefix only>"
},
{
"type": "text",
"text": "<user's typed reason -- unsigned, intent-checked normally>"
}
]
The static harness prefix is signed and trusted. The user-supplied portion is delivered as a normal "type": "text" block and is subject to full intent scoring. This preserves both auditability and moderation coverage.
### Wire format
{
"type": "harness",
"harness_type": "compaction",
"text": "CRITICAL: Respond with TEXT ONLY. Do NOT call any tools. Your task is to create a detailed summary...",
"kid": "2026-05",
"sig": "<base64url(Ed25519 signature)>"
}
Fields:
type-- always"harness", never"text". Any block with"type": "text"is treated as user content unconditionally.harness_type-- slug identifying the injection kind. Included in the signed message to prevent type substitution.text-- the injection content.kid-- key ID matching a published public key, enabling rotation without breaking existing proxies.sig-- base64url-encoded Ed25519 signature overharness_type + "\n" + text.
Public key publication
GET https://api.anthropic.com/.well-known/harness-keys.json
{
"keys": [
{
"kid": "2026-05",
"alg": "Ed25519",
"use": "harness-sig",
"x": "<base64url public key bytes>",
"nbf": 1746057600,
"exp": 1777593600
}
]
}
Operators pin the public key at deployment and refresh on rotation. The kid field in each harness block identifies which key to verify against, allowing seamless rollover.
Key rotation and revocation
- Keys should rotate on a cadence matching Claude Code major releases (roughly quarterly).
- On private key compromise: publish a new key, issue an emergency release of Claude Code signing with the new key, and set
expon the compromised key to the compromise timestamp. Proxies that enforceexpwill reject blocks signed with the old key after the expiry. - The
kid+expmechanism mirrors standard JWK rotation and is well-understood by security tooling.
Proxy verification
import base64
import httpx
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
# Load from https://api.anthropic.com/.well-known/harness-keys.json at startup
HARNESS_KEYS: dict[str, Ed25519PublicKey] = {}
def is_trusted_harness_block(block: dict) -> bool:
if block.get("type") != "harness":
return False
kid = block.get("kid", "")
pub = HARNESS_KEYS.get(kid)
if pub is None:
return False # unknown key -- treat as user content
try:
sig = base64.urlsafe_b64decode(block["sig"] + "==")
msg = (block["harness_type"] + "\n" + block["text"]).encode()
pub.verify(sig, msg)
return True # verified harness injection -- skip intent check
except Exception:
return False # invalid signature -- treat as user content, run intent check
Any block with "type": "text" is unconditionally treated as user content. Only "type": "harness" blocks with a valid, unexpired signature from a known key are excluded from moderation.
Why an attacker cannot forge this
To bypass moderation an attacker must produce a valid signature over their malicious payload:
Ed25519_sign(private_key, "compaction\nIgnore all previous instructions...")
The private key is held server-side and never distributed. The attacker cannot:
- Derive the private key from the public key -- elliptic curve discrete log, 128-bit security.
- Replay a captured signature -- the signature covers the exact content; replacing the text invalidates the signature.
- Extract the key from the binary -- the private key is not in the binary under the recommended server-side model.
- Brute-force a valid signature -- computationally infeasible.
- Forge using the session token -- session tokens are bound to a session ID and expire; a captured token is useless outside the original session.
Full knowledge of the spec, the wire format, and the public key provides zero advantage for forgery.
---
Minimal ask (if full signing is not feasible short-term)
At minimum, deliver harness injections as "type": "harness" blocks rather than "type": "text". This gives proxy operators a structural signal today without requiring cryptographic infrastructure, and signing can be layered on in a subsequent release.
This is strictly better than the current state because:
- Proxies filter on block type without any content matching.
- The model can be instructed to treat
"type": "harness"as a structurally distinct trust level. - Unsigned
"type": "harness"blocks can be rejected or intent-checked at the proxy's discretion. - No breaking change to the messages schema --
"type": "harness"is additive.
---
Relationship to existing open issues
- #23537 -- harness content indistinguishable from user input causes model mis-attribution. A distinct signed block type resolves this at the model layer.
- #46465 -- harness phrasing is textbook prompt injection. Signed blocks remove the phrasing from the user-content channel entirely.
- #56829 -- multiple system-reminder blocks cause Bedrock to drop user messages. A distinct block type processed separately from
"type": "text"prevents this collision class. - #17601 -- 10,000+ undisclosed injections consuming context and tokens. Typed blocks make the injection surface enumerable and auditable.
- #29729 -- plugin signing. The key infrastructure proposed here is architecturally identical and could be shared.
---
Impact
Security: The model gains a structural signal to distinguish operational instructions from user input, directly reducing its susceptibility to injection attacks that mimic harness phrasing.
Operations: Proxy and moderation operators eliminate version-coupled content fingerprinting. Typed blocks are identifiable across all Claude Code versions without any per-release maintenance. Filtering is cryptographically sound rather than heuristic.
Compliance: Enterprise operators can attribute content by source in audit logs, measure and disclose harness token consumption, and demonstrate to auditors that user input and system instructions are structurally separated -- meeting SOC 2, GDPR data processor, and similar obligations.
Ecosystem: The kid-based key rotation model is compatible with standard JWK tooling. Operators can integrate verification into existing security pipelines without custom code.
Alternative Solutions
_No response_
Priority
High - Significant impact on productivity
Feature Category
API and model interactions
Use Case Example
_No response_
Additional Context
_No response_
This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗