Claude-Agent
view release on metacpan or search on metacpan
lib/Claude/Agent/DryRun.pm view on Meta::CPAN
scalar(@filtered), join(', ', map { "'$_'" } @filtered));
}
my %safe_commands = map { $_ => 1 } @valid_commands;
# Extract first command word (before any args, pipes, or redirects)
my ($first_cmd) = $command =~ /^\s*(\S+)/;
$first_cmd //= '';
# Keep full path for safe directory validation
if ($first_cmd =~ m{^/}) {
# Only strip if it's in a known safe directory
return 1 unless $first_cmd =~ m{^/(usr/)?s?bin/};
}
$first_cmd =~ s{^.*/}{};
# Block if command is not in safe list
return 1 unless $safe_commands{$first_cmd};
# Even safe commands are blocked if they use redirects or pipes
return 1 if $command =~ /[>|]/;
return 0; # Safe command without redirects - allow
}
# DEPRECATED: Heuristic fallback mode - will be removed in a future version
# Emit deprecation warning when heuristic mode is used
state $heuristic_warned = 0;
if (!$heuristic_warned++) {
warn "[DEPRECATION WARNING] Dry-run heuristic mode (CLAUDE_AGENT_DRY_RUN_STRICT=0) is "
. "deprecated and provides NO security guarantees. This fallback may be removed in "
. "future versions. Use strict mode (default) for any security-sensitive applications.\n";
}
# Commands that are definitely writes
# Note: This detection is not exhaustive. Complex shell constructs, xargs with
# write commands, or custom scripts may bypass detection. For maximum safety,
# consider using a whitelist approach or reviewing all blocked operations.
#
# IMPORTANT: Dry-run mode provides INFORMATIONAL protection only, NOT security
# guarantees. This regex-based detection can be bypassed using shell obfuscation
# techniques including:
# - Command substitution: $(echo rm) -rf
# - Variable expansion: $cmd where cmd=rm
# - Hex/octal encoding in certain shells
# - Aliases that expand to write commands
# - Less common destructive tools (shred, truncate via other means)
# - Custom scripts that perform write operations
#
# For security-critical applications, consider:
# 1. Set CLAUDE_AGENT_DRY_RUN_STRICT=1 to block all non-whitelisted commands
# 2. Running in a sandboxed environment (containers, VMs)
# 3. Using custom PreToolUse hooks with stricter validation
#
# WARNING: Always print security notice to STDERR for Bash commands
# This ensures users are aware of limitations even if callbacks suppress output
if (!$ENV{CLAUDE_AGENT_DRY_RUN_QUIET}) {
state $dry_run_warned = 0;
warn "[DRY-RUN WARNING] Bash command detection is bypassable. "
. "Set CLAUDE_AGENT_DRY_RUN_STRICT=1 for stricter protection.\n"
unless $dry_run_warned++;
}
# More precise command detection: check if dangerous command is at start or after pipe/semicolon/&&
# This avoids false positives like 'grep rm file.txt' or 'echo rm > log.txt'
my @dangerous_cmds = qw(rm rmdir mv cp mkdir touch chmod chown dd truncate install ln patch rsync shred);
for my $cmd (@dangerous_cmds) {
return 1 if $command =~ /^\s*$cmd\b/ || $command =~ /[;|&]\s*$cmd\b/;
}
# Handle wget and curl with output flags separately (more complex patterns)
return 1 if $command =~ /^\s*wget\b/ || $command =~ /[;|&]\s*wget\b/;
return 1 if $command =~ /^\s*curl\s+.*-[oO]/ || $command =~ /[;|&]\s*curl\s+.*-[oO]/;
return 1 if $command =~ /<<[<]?/; # Heredoc redirects
return 1 if $command =~ /\b(perl|python|ruby|sh|bash)\s+(-[ec]|-.*[ec])/i; # Inline scripts that could write
return 1 if $command =~ /\beval\b/; # eval command
return 1 if $command =~ /\b(source|\.)\s+/; # source command
return 1 if $command =~ /\bxargs\b.*\b(rm|mv|cp)\b/; # xargs with write commands
return 1 if $command =~ /\b(git\s+(add|commit|push|reset|checkout|merge|rebase))\b/;
return 1 if $command =~ /\b(npm\s+(install|uninstall|update|publish))\b/;
return 1 if $command =~ /\b(pip\s+(install|uninstall))\b/;
return 1 if $command =~ /\b(cargo\s+(build|install|publish))\b/;
return 1 if $command =~ /\b(make|cmake)\b/;
return 1 if $command =~ /[>|]\s*\S/; # Redirects or pipes to commands
return 1 if $command =~ /`[^`]*[>|]/; # Command substitution with backticks
return 1 if $command =~ /\$\([^)]*[>|]/; # Command substitution with $()
return 1 if $command =~ /\btee\b/;
return 1 if $command =~ /\bsed\s+-i/; # In-place sed
# If disabling sandbox, likely a write
return 1 if $tool_input->{dangerouslyDisableSandbox};
# In non-strict mode, warn if command is not in the safe list
# This helps users understand what would be blocked in strict mode
if (!$ENV{CLAUDE_AGENT_DRY_RUN_QUIET}) {
my $safe_commands_env = $ENV{CLAUDE_AGENT_DRY_RUN_SAFE_COMMANDS}
// 'ls,cat,head,tail,grep,find,which,pwd,whoami,date,echo,wc,file,stat,type,uname,env,printenv';
my %safe_commands = map { $_ => 1 } split /,/, $safe_commands_env;
my ($first_cmd) = $command =~ /^\s*(\S+)/;
$first_cmd //= '';
$first_cmd =~ s{^.*/}{};
if (!$safe_commands{$first_cmd}) {
warn "[DRY-RUN NOTICE] Command '$first_cmd' is not in safe list but allowed by heuristics. "
. "In strict mode (default), this would be blocked.\n";
}
}
}
# MCP tools - check if marked as write operation via configuration
# Users can specify MCP tools that perform write operations via the
# CLAUDE_AGENT_MCP_WRITE_TOOLS environment variable (comma-separated list)
# or by implementing custom PreToolUse hooks.
if ($tool_name =~ /^mcp__/) {
# Check if tool is in user-configured write-tools list
my $write_tools_env = $ENV{CLAUDE_AGENT_MCP_WRITE_TOOLS} // '';
my %write_mcp_tools = map { $_ => 1 } split /,/, $write_tools_env;
return 1 if $write_mcp_tools{$tool_name};
return 0;
}
# Default: allow (read-only tools like Read, Glob, Grep, etc.)
return 0;
}
=head2 format_preview
my $preview = format_preview($tool_name, $tool_input);
( run in 1.572 second using v1.01-cache-2.11-cpan-71847e10f99 )