10 KiB
Security Documentation
This document describes the security measures implemented in the Claude Agent SDK for Rust.
Overview
The SDK implements multiple layers of defense to ensure secure operation when interfacing with the Claude Code CLI.
Security Measures
1. Environment Variable Blocking (Strict Enforcement)
Dangerous environment variables that could be exploited for code injection cause an immediate error - they are not silently filtered:
const DANGEROUS_ENV_VARS: &[&str] = &[
"LD_PRELOAD", // Linux dynamic linker injection
"LD_LIBRARY_PATH", // Linux library path manipulation
"DYLD_INSERT_LIBRARIES", // macOS dylib injection
"DYLD_LIBRARY_PATH", // macOS library path manipulation
"PATH", // Command search path manipulation
"NODE_OPTIONS", // Node.js runtime injection
"PYTHONPATH", // Python module injection
"PERL5LIB", // Perl module injection
"RUBYLIB", // Ruby library injection
];
Behavior:
- Safe env vars: Passed to subprocess normally
- Dangerous env vars: Returns
ClaudeError::InvalidConfigwith details - Logs warning (with
tracing-supportfeature)
// This will fail with an error
options.env.insert("LD_PRELOAD".to_string(), "/tmp/evil.so".to_string());
let client = ClaudeSDKClient::new(options, None).await;
// Error: "Dangerous environment variables detected: [LD_PRELOAD]. These are blocked to prevent injection attacks."
Location: src/transport/subprocess.rs:23-33, enforced at :549-572
2. CLI Argument Allowlist (Strict Enforcement)
Only explicitly allowed CLI flags can be passed via extra_args. Disallowed flags cause an immediate error - they are not silently ignored:
const ALLOWED_EXTRA_FLAGS: &[&str] = &["timeout", "retries", "log-level", "cache-dir"];
Behavior:
- Allowed flags: Passed to CLI normally
- Disallowed flags: Returns
ClaudeError::InvalidConfigwith details - Logs warning (with
tracing-supportfeature)
// This will fail with an error
options.extra_args.insert("dangerous-flag".to_string(), None);
let client = ClaudeSDKClient::new(options, None).await;
// Error: "Disallowed CLI flags in extra_args: [dangerous-flag]. Allowed flags: [...]"
Location: src/transport/subprocess.rs:36, enforced at :426-461
Validation Order and Precedence
Both checks run during connect(). CLI args are validated first, then env vars.
CLI args use an allowlist (whitelist):
- Only flags explicitly listed in
ALLOWED_EXTRA_FLAGSare permitted - Everything else is rejected
Env vars use a blocklist (blacklist):
- Only vars listed in
DANGEROUS_ENV_VARSare rejected - Everything else is permitted
There is no way to override these at runtime. The lists are compile-time constants. Attempting to pass a blocked item always fails - there is no "force" option by design.
3. Buffer Size Limits
Memory exhaustion attacks are prevented by limiting JSON message sizes:
- Default limit: 1MB (
DEFAULT_MAX_BUFFER_SIZE = 1024 * 1024) - Configurable: Via
ClaudeAgentOptions::max_buffer_size - Messages exceeding the limit generate an error and are discarded
Location: src/transport/subprocess.rs:20, enforced at subprocess.rs:688-699
4. Timeout Strategy (CancellationToken-based)
The SDK uses cooperative cancellation instead of hardcoded timeouts for I/O operations.
Design Rationale
Hardcoded timeouts (e.g., 30-second read timeouts) were considered but rejected because:
- Long-running operations are legitimate: Claude Code queries involving subagent research, complex code analysis, or multi-step agentic workflows can take minutes
- False positive risk: A timeout would interrupt valid operations, corrupting state
- User control is preferable: Applications know their expected operation durations better than the SDK
Implementation
The SDK implements CancellationToken (analogous to JavaScript's AbortController):
// Client provides cancellation control
let token = client.cancellation_token();
// Application can cancel at any time
client.cancel();
// Background tasks respect cancellation
tokio::select! {
_ = cancel_token.cancelled() => { /* cleanup */ }
result = read_operation => { /* process */ }
}
Locations:
ClaudeSDKClient::cancellation_token()- Get a child tokenClaudeSDKClient::cancel()- Cancel all operationsSubprocessTransport::read_messages()- Respects cancellation in read loop
Graceful Shutdown
The close() method implements a graceful shutdown with a 5-second timeout before forcing process termination:
// Try graceful exit first
let timeout_duration = Duration::from_secs(5);
match tokio::time::timeout(timeout_duration, child.wait()).await {
Ok(_) => { /* graceful exit */ }
Err(_) => {
child.kill().await; // Force kill after timeout
}
}
Location: src/transport/subprocess.rs:792-807
5. No Unsafe Code
The SDK is 100% safe Rust with no unsafe blocks. This ensures memory safety guarantees are enforced by the compiler.
6. Input Bounds Checking
Configurable values are bounded to prevent abuse:
max_turns: Maximum 1000 turns (ClaudeAgentOptions::MAX_ALLOWED_TURNS)
7. Secure Debug Output
Sensitive data (callbacks, hooks) are redacted in Debug implementations:
impl Debug for ClaudeAgentOptions {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.debug_struct("ClaudeAgentOptions")
.field("can_use_tool", &self.can_use_tool.as_ref().map(|_| "<callback>"))
// ...
}
}
Location: src/types/options.rs:442-514
8. Stderr Isolation
The subprocess stderr is piped (not inherited) to prevent the child process from manipulating the parent terminal state:
cmd.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped()); // Prevents terminal manipulation
Location: src/transport/subprocess.rs:538-540
9. Session Binding (Secure by Default)
The SDK automatically binds to a session on the first Result message, preventing messages from being sent to unintended conversation contexts.
Design Rationale
Without session binding, a multi-turn application could accidentally send messages to a different session if:
- Session IDs get mixed up in application state
- Race conditions in concurrent applications
- Unexpected session restarts
Implementation
Session binding is automatic and secure by default:
// Auto-binds on first Result - no manual binding required
client.send_message("Hello").await?;
while let Some(msg) = client.next_message().await {
if let Message::Result { .. } = msg? {
// Session is now auto-bound!
break;
}
}
// All subsequent sends validate automatically
client.send_message("Follow-up").await?; // Validates session first
If the current session doesn't match the bound session, send_message() returns ClaudeError::SessionMismatch:
#[error("Session mismatch: expected {expected}, got {actual}")]
SessionMismatch {
expected: String,
actual: String,
}
Override Methods
For advanced use cases (e.g., multi-session applications):
| Method | Description |
|---|---|
bind_session(id) |
Override auto-bound session |
unbind_session() |
Disable validation (use with caution) |
validate_session() |
Manual validation check |
bound_session() |
Get currently bound session |
Subagent Behavior
Subagents (via Task tool) share the parent session ID, so auto-bind works correctly:
- Parent binds to session on first Result
- Subagent hooks report same session ID
- No SessionMismatch conflicts
Locations:
- Auto-bind:
src/client.rs:449-454(inmessage_reader_task) - Validation:
src/client.rs(validate_session(),send_message()) - Error type:
src/error.rs:100-107
10. OAuth Token Security
The OAuth module stores authentication tokens with appropriate security measures:
Token Storage
- Location: Platform-specific config directory
- macOS:
~/Library/Application Support/claude-sdk/oauth_token.json - Linux:
~/.config/claude-sdk/oauth_token.json - Windows:
%APPDATA%\claude-sdk\oauth_token.json
- macOS:
- Permissions:
0600on Unix (owner read/write only) - Contents: Access token, refresh token, expiration timestamp
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let perms = std::fs::Permissions::from_mode(0o600);
std::fs::set_permissions(&storage_path, perms)?;
}
Token Handling
- Expiration buffer: Tokens considered expired 60 seconds before actual expiry
- Auto-refresh: Expired tokens trigger automatic refresh via refresh_token
- PKCE: Authorization uses PKCE (SHA-256 challenge) to prevent code interception
- No secrets in logs: Token values are truncated in display output
Logout
The logout() method deletes the cached token file entirely.
Location: src/auth/token.rs:172-213, src/auth/oauth.rs
Threat Model
| Threat | Mitigation |
|---|---|
| CLI command injection | Argument allowlist, environment filtering |
| Memory exhaustion | Buffer size limits |
| Infinite hangs | CancellationToken, graceful shutdown timeout |
| Terminal corruption | Stderr isolation |
| Memory unsafety | 100% safe Rust |
| Session confusion/hijacking | Auto-bind on first Result, validation on send |
| OAuth token theft | Restrictive file permissions (0600), PKCE for auth |
| OAuth code interception | PKCE with SHA-256 challenge/verifier |
Reporting Security Issues
Please report security vulnerabilities to the maintainers via GitHub security advisories.