Claude-skill-registry agentic-jumpstart-security
Security best practices and guidelines for the Jarvy CLI codebase - a cross-platform development environment provisioning tool that executes system commands with elevated privileges
git clone https://github.com/majiayu000/claude-skill-registry
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/agentic-jumpstart-security" ~/.claude/skills/majiayu000-claude-skill-registry-agentic-jumpstart-security && rm -rf "$T"
skills/data/agentic-jumpstart-security/SKILL.md- curl piped into shell
- uses sudo
- makes HTTP requests (curl)
Security Guidelines for Jarvy CLI
This skill provides comprehensive security guidelines for developing and maintaining the Jarvy CLI codebase. Jarvy is a cross-platform tool that provisions development environments by executing package manager commands (brew, apt, dnf, winget, etc.) with potential privilege escalation.
Threat Model Overview
Jarvy operates in a high-risk security context:
- Command Execution: Executes external binaries (package managers, shells) with user-supplied data
- Privilege Escalation: Uses
on POSIX systems for package installationsudo - Configuration Parsing: Reads TOML files from user-specified paths
- Network Requests: Makes HTTP requests to PostHog and OTLP endpoints
- Telemetry Collection: Gathers machine fingerprints and usage data
- Shell Script Execution: Downloads and executes installer scripts (rustup, nvm)
1. Command Injection Prevention
The Risk
Jarvy passes user-provided tool names and versions to package manager commands. A malicious
jarvy.toml could attempt command injection.
CORRECT Pattern: Argument Array Separation
Always use
Command::new() with separate arguments - NEVER shell string interpolation:
// CORRECT: Arguments are passed as separate array elements // Each argument is escaped by the OS, preventing injection use std::process::Command; pub fn run(cmd: &str, args: &[&str]) -> Result<Output, InstallError> { Command::new(cmd) .args(args) // Safe: each arg is a separate element .output() .map_err(|e| /* handle error */) } // Usage - safe even with untrusted input run("brew", &["install", user_provided_package_name])?; run("apt-get", &["install", "-y", package_name])?; run("winget", &["install", "-e", "--id", package_id])?;
ANTI-PATTERN: Shell String Interpolation
// DANGEROUS: Never concatenate user input into shell commands let cmd = format!("brew install {}", user_input); // VULNERABLE Command::new("sh").args(["-c", &cmd]).output()?; // VULNERABLE // DANGEROUS: Using shell expansion with user data let script = format!("curl ... | sh -s -- {}", user_version); // VULNERABLE
CORRECT Pattern: Hardcoded Package IDs
For package managers that use IDs, hardcode known-safe values:
// CORRECT: Package IDs are compile-time constants const GIT_WINGET_ID: &str = "Git.Git"; const RUSTUP_WINGET_ID: &str = "Rustlang.Rustup"; fn install_windows() -> Result<(), InstallError> { run("winget", &["install", "-e", "--id", GIT_WINGET_ID])?; Ok(()) }
CORRECT Pattern: Input Validation for Tool Names
Validate tool names against an allowlist before any operations:
// CORRECT: Only process tools registered in the known registry pub fn add(name: &str, version: &str) -> Result<(), InstallError> { let key = name.to_ascii_lowercase(); // Normalize let map = registry().read().expect("registry rwlock poisoned"); if let Some(handler) = map.get(&key) { // Only known tools can be processed let f = *handler; drop(map); f(version) } else { Err(InstallError::Parse("unknown tool")) // Reject unknown tools } }
Version String Validation
Version hints should be validated to prevent injection via version parameters:
// CORRECT: Validate version strings contain only expected characters fn validate_version_hint(hint: &str) -> bool { // Allow: digits, dots, hyphens, plus signs (semver compatible) hint.chars().all(|c| c.is_ascii_alphanumeric() || matches!(c, '.' | '-' | '+')) } // CORRECT: Use version for comparison only, not command construction pub fn cmd_satisfies(cmd: &str, min_prefix: &str) -> bool { if let Ok(out) = Command::new(cmd).arg("--version").output() { let s = String::from_utf8_lossy(&out.stdout); return s.contains(min_prefix); // Read-only comparison } false }
2. Privilege Escalation Security
The Risk
Improper sudo handling can lead to privilege escalation attacks or unintended system modifications.
CORRECT Pattern: Explicit Sudo Control
// CORRECT: Sudo is a conscious decision, not automatic pub fn run_maybe_sudo(use_sudo: bool, cmd: &str, args: &[&str]) -> Result<Output, InstallError> { match current_os() { Os::Windows => run(cmd, args), // No sudo on Windows Os::Linux | Os::Macos => { if use_sudo { // Prepend sudo as a separate command let mut all = Vec::with_capacity(1 + args.len()); all.push(cmd); all.extend_from_slice(args); run("sudo", &all) // sudo is the command, original cmd is first arg } else { run(cmd, args) } } } }
CORRECT Pattern: Sudo Detection and User Configuration
// CORRECT: Check if sudo is available before attempting pub fn has(cmd: &str) -> bool { Command::new(cmd) .arg("--version") .output() .map(|o| o.status.success()) .unwrap_or(false) } // CORRECT: Allow users to control sudo behavior via config pub fn plan_sudo_attempts(use_sudo: Option<bool>, sudo_available: bool) -> Vec<bool> { match use_sudo { Some(flag) => vec![flag], // User explicitly set preference None => { if sudo_available { vec![false, true] // Try without first, then with } else { vec![false] // No sudo available } } } }
ANTI-PATTERN: Unconditional Sudo
// DANGEROUS: Always using sudo without user consent fn install_package(pkg: &str) -> Result<(), Error> { run("sudo", &["apt-get", "install", pkg])?; // WRONG: No user control Ok(()) } // DANGEROUS: Sudo in shell strings let cmd = "sudo apt-get install -y package"; // WRONG: Shell injection risk Command::new("sh").args(["-c", cmd]).output()?;
Permission Error Handling
// CORRECT: Distinct error types for permission issues #[derive(thiserror::Error, Debug)] pub enum InstallError { #[error("invalid permissions: {0}")] InvalidPermissions(&'static str), // ... } // CORRECT: Map permission errors to specific exit codes match e.kind() { std::io::ErrorKind::PermissionDenied => { InstallError::InvalidPermissions("operation requires elevated privileges") } // ... }
3. Path Traversal Prevention
The Risk
User-specified config file paths could access sensitive system files.
CORRECT Pattern: Path Canonicalization
use std::path::Path; // CORRECT: Canonicalize and validate paths fn safe_read_config(path: &str) -> Result<String, ConfigError> { let path = Path::new(path); // Canonicalize to resolve symlinks and ../ sequences let canonical = path.canonicalize() .map_err(|_| ConfigError::InvalidPath)?; // Ensure file has expected extension if canonical.extension() != Some(std::ffi::OsStr::new("toml")) { return Err(ConfigError::InvalidExtension); } std::fs::read_to_string(canonical) .map_err(ConfigError::IoError) }
CORRECT Pattern: Restricted Config Locations
// CORRECT: Global config is always in a known location pub fn initialize() -> CliConfig { let home_dir = dirs::home_dir().expect("Failed to get home directory"); let jarvy_dir = home_dir.join(".jarvy"); // Fixed subdirectory let config_file_path = jarvy_dir.join("config.toml"); // Fixed filename // Only read from this specific path let config_content = fs::read_to_string(&config_file_path).unwrap_or_default(); // ... }
ANTI-PATTERN: Unrestricted Path Access
// DANGEROUS: Directly using user path without validation fn read_config(path: &str) -> String { fs::read_to_string(path).unwrap() // WRONG: No validation } // DANGEROUS: String concatenation for paths let path = format!("{}/{}", user_dir, user_filename); // WRONG: Path injection
4. Telemetry Data Security
The Risk
Telemetry could leak PII, credentials, or sensitive system information.
CORRECT Pattern: Anonymized Identifiers
// CORRECT: Use hardware-based anonymous fingerprint fn get_hwid_fingerprint() -> Result<String, Box<dyn std::error::Error>> { let mut builder = IdBuilder::new(Encryption::SHA256); // Components that identify the machine, not the user builder .add_component(HWIDComponent::SystemID) .add_component(HWIDComponent::CPUCores) .add_component(HWIDComponent::OSName) .add_component(HWIDComponent::DriveSerial); // Salted hash - cannot be reversed to original values const SALT: &str = "9f86d081884c7d659a2feaa0c55ad015..."; builder.build(SALT) }
CORRECT Pattern: Explicit Opt-Out
// CORRECT: Allow users to disable telemetry pub fn init(enable_analytics: bool, distinct_id: String) { // Environment variable override let env_disable = std::env::var("JARVY_ANALYTICS").ok(); let enabled = match env_disable.as_deref() { Some("0") | Some("false") => false, // Honor user preference _ => enable_analytics, }; // Clear documentation about telemetry println!(r" Jarvy collects telemetry data to help us improve your experience. The data collected is anonymized... If you wish to opt-out, add to ~/.jarvy/config.toml: [settings] telemetry = false "); }
Data Minimization
// CORRECT: Only collect necessary, non-sensitive data pub fn capture(event: &str, mut properties: serde_json::Map<String, serde_json::Value>) { // Safe to collect: OS type, shell type, CLI version properties.entry("os".to_string()) .or_insert(serde_json::Value::String(detect_os())); properties.entry("shell".to_string()) .or_insert(serde_json::Value::String(detect_shell())); properties.entry("version".to_string()) .or_insert(serde_json::Value::String(env!("CARGO_PKG_VERSION").to_string())); // DO NOT collect: usernames, paths containing usernames, environment variables // containing secrets, command arguments that might contain credentials }
ANTI-PATTERN: PII in Telemetry
// DANGEROUS: Collecting potentially sensitive data properties.insert("username", std::env::var("USER").unwrap()); // WRONG: PII properties.insert("home_dir", dirs::home_dir().display()); // WRONG: Contains username properties.insert("full_command", std::env::args().collect()); // WRONG: May contain secrets properties.insert("env_vars", std::env::vars().collect()); // WRONG: Contains secrets
5. HTTP Request Security
The Risk
Network requests could be intercepted (MITM) or directed to malicious endpoints.
CORRECT Pattern: HTTPS Only with Certificate Validation
// CORRECT: Use HTTPS endpoints with proper certificate validation // ureq performs certificate validation by default let response = ureq::post(&format!("{}/capture/", c.host)) // host should be https:// .header("Content-Type", "application/json") .send_json(payload)?; // CORRECT: Hardcode HTTPS URLs for installers const RUSTUP_URL: &str = "https://sh.rustup.rs"; run("bash", &["-lc", "curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y" ])?;
CORRECT Pattern: TLS Version Enforcement
// CORRECT: Enforce modern TLS in curl commands "curl --proto '=https' --tlsv1.2 -sSf https://example.com" // ^^^^^^^^^^^^^^^ only HTTPS // ^^^^^^^^^^^ TLS 1.2 minimum
ANTI-PATTERN: Insecure Network Requests
// DANGEROUS: HTTP endpoints let host = "http://api.example.com"; // WRONG: No TLS // DANGEROUS: Disabling certificate verification // (ureq doesn't easily allow this, but some HTTP clients do) // DANGEROUS: Downloading scripts without verification run("bash", &["-c", "curl http://example.com/script.sh | bash"])?; // WRONG: HTTP + piped execution
6. Safe External Command Output Parsing
The Risk
Parsing command output incorrectly can lead to logic errors or information disclosure.
CORRECT Pattern: Lossy UTF-8 Conversion
// CORRECT: Use lossy conversion to handle malformed output safely if !out.status.success() { return Err(InstallError::CommandFailed { cmd: cmd.to_string(), code: out.status.code(), stderr: String::from_utf8_lossy(&out.stderr).into(), // Safe conversion }); } // CORRECT: Version checking with contains (not parsing) pub fn cmd_satisfies(cmd: &str, min_prefix: &str) -> bool { if let Ok(out) = Command::new(cmd).arg("--version").output() { let s = String::from_utf8_lossy(&out.stdout); return s.contains(min_prefix); // Simple substring check } false }
CORRECT Pattern: Defensive Parsing
// CORRECT: Handle parsing failures gracefully let config: CliConfig = { let config_content = fs::read_to_string(&config_file_path).unwrap_or_default(); if config_content.trim().is_empty() { CliConfig::default() // Safe fallback } else { toml::from_str(&config_content).unwrap_or_default() // Safe fallback on parse error } };
ANTI-PATTERN: Unsafe Parsing
// DANGEROUS: Assuming output is valid UTF-8 let output = String::from_utf8(out.stdout).unwrap(); // WRONG: Can panic // DANGEROUS: Complex regex on untrusted output without limits let re = Regex::new(r"version (\d+)\.(\d+)\.(\d+)").unwrap(); // Consider: What if output is 10MB of garbage? // DANGEROUS: Executing parsed output let version = parse_version(&output); run("install", &[&format!("package@{}", version)])?; // WRONG: Trusting parsed data
7. Script Download and Execution Security
The Risk
Downloading and executing scripts from the internet is inherently dangerous.
CORRECT Pattern: Pinned Versions and HTTPS
// CORRECT: Pin to specific versions when downloading scripts const NVM_VERSION: &str = "v0.39.7"; const NVM_INSTALL_URL: &str = "https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh"; fn install_posix() -> Result<(), InstallError> { run("bash", &["-lc", &format!("curl -fsSL {} | bash", NVM_INSTALL_URL) ])?; Ok(()) }
CORRECT Pattern: Verify Script Source
// CORRECT: Only use official, well-known installer URLs // rustup: https://sh.rustup.rs (official Rust project) // nvm: https://raw.githubusercontent.com/nvm-sh/nvm/... (official repo) // Homebrew: https://raw.githubusercontent.com/Homebrew/install/... // Document the trust chain in comments /// Install Rust via rustup using the official installer. /// Source: https://rustup.rs (maintained by Rust project) fn install_unix() -> Result<(), InstallError> { run("bash", &["-lc", "curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y" ]) }
ANTI-PATTERN: Arbitrary Script Execution
// DANGEROUS: User-provided URLs fn install_from_url(url: &str) -> Result<(), Error> { run("bash", &["-c", &format!("curl {} | bash", url)])?; // WRONG Ok(()) } // DANGEROUS: Unpinned versions "curl -fsSL https://example.com/install.sh | bash" // WRONG: Gets latest, could change
8. Error Handling and Information Disclosure
The Risk
Detailed error messages can leak sensitive system information.
CORRECT Pattern: Structured Errors Without Sensitive Data
// CORRECT: Error types that don't leak paths or system details #[derive(thiserror::Error, Debug)] pub enum InstallError { #[error("unsupported platform")] Unsupported, #[error("prerequisite missing: {0}")] Prereq(&'static str), // Static string, not dynamic path #[error("invalid permissions: {0}")] InvalidPermissions(&'static str), #[error("command failed: {cmd} (code: {code:?})\n{stderr}")] CommandFailed { cmd: String, // Command name only code: Option<i32>, stderr: String, // May contain sensitive info - log carefully }, }
CORRECT Pattern: Safe Error Logging to Telemetry
// CORRECT: Sanitize errors before sending to telemetry pub fn capture_exception( message: &str, exception_type: &str, stack_trace: Option<String>, context: serde_json::Map<String, serde_json::Value>, ) { // Filter or redact sensitive information from stack traces let safe_stack = stack_trace.map(|s| redact_paths(&s)); // Don't include full file paths in telemetry let mut safe_context = context.clone(); safe_context.remove("full_path"); safe_context.remove("home_directory"); // Send sanitized data capture("$exception", props); }
ANTI-PATTERN: Verbose Error Messages
// DANGEROUS: Including full paths in errors eprintln!("Failed to read: {}", full_path); // WRONG: Exposes filesystem structure // DANGEROUS: Including environment in errors eprintln!("Error with env: {:?}", std::env::vars()); // WRONG: Leaks secrets // DANGEROUS: Stack traces to users panic!("Unexpected error: {:?}", e); // WRONG: May contain sensitive info
9. Dependency Security
Guidelines
- Minimize Dependencies: Prefer stdlib over external crates when feasible
- Audit Dependencies: Use
andcargo audit
regularlycargo deny - Pin Versions: Use exact versions in Cargo.toml for security-critical dependencies
- Review Features: Only enable necessary crate features
Current Dependencies Security Notes
# Security-relevant dependencies in Jarvy: # ureq - HTTP client # - Uses rustls by default (no OpenSSL dependency issues) # - Certificate validation enabled by default ureq = { version = "3.1.2", features = ["json"] } # serde/toml - Config parsing # - Well-audited, widely used # - Deserialize untrusted input with care serde = { version = "1.0.204", features = ["derive"] } toml = "0.9.5" # machineid-rs - Hardware fingerprinting # - Review what data it collects # - Ensure it doesn't collect PII machineid-rs = "1.2"
10. Testing Security
CORRECT Pattern: Test Mode Isolation
// CORRECT: Disable dangerous operations in tests pub fn run(cmd: &str, args: &[&str]) -> Result<Output, InstallError> { // Fast tests skip external commands entirely if std::env::var_os("JARVY_FAST_TEST").is_some() { return Err(InstallError::Prereq("skipped in fast test mode")); } // Unit tests don't run external commands by default #[cfg(test)] { if std::env::var_os("JARVY_RUN_EXTERNAL_CMDS_IN_TEST").is_none() { return Err(InstallError::Prereq("external commands disabled in tests")); } } // Actual command execution... } // CORRECT: Test mode avoids interactive prompts fn user_select() { if std::env::var("JARVY_TEST_MODE").as_deref() == Ok("1") { println!("TEST: user_select invoked"); return; // Skip interactive UI } // ... }
Security Checklist for Code Reviews
When reviewing PRs, verify:
- No user input is interpolated into shell command strings
- All
calls use argument arrays, not concatenated stringsCommand::new() - Sudo usage is controlled by user configuration
- Config file paths are validated/canonicalized
- Telemetry events don't contain PII or secrets
- HTTP requests use HTTPS with certificate validation
- Downloaded scripts are from pinned, official sources
- Error messages don't leak sensitive paths or environment data
- New dependencies are audited for security
- Tests don't execute real system commands without explicit opt-in
Reporting Security Issues
If you discover a security vulnerability in Jarvy:
- Do not open a public GitHub issue
- Email security concerns to the maintainers privately
- Include:
- Description of the vulnerability
- Steps to reproduce
- Potential impact assessment
- Suggested fix (if any)