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 )