Acme-Claude-Shell
view release on metacpan or search on metacpan
lib/Acme/Claude/Shell/Tools.pm view on Meta::CPAN
=back
=head2 Command Approval
The C<execute_command> tool handles user approval directly (not via hooks)
to ensure synchronous confirmation before execution. Users can:
=over 4
=item * B<[a]> Approve and run the command
=item * B<[d]> Dry-run (preview only, don't execute)
=item * B<[e]> Edit the command before running
=item * B<[x]> Cancel
=back
=head2 Dangerous Command Detection
The following patterns trigger additional safety warnings:
=over 4
=item * C<rm -rf>, C<rm --recursive>, C<rm --force>
=item * C<sudo> commands
=item * C<mkfs>, C<dd of=>, device writes
=item * C<chmod 777>, C<chown -R>
=item * C<kill -9>, C<reboot>, C<shutdown>, C<halt>, C<poweroff>
=item * Fork bombs, remote script piping (curl/wget | sh)
=back
=cut
sub shell_tools {
my ($session) = @_;
return [
# execute_command tool - ALL shell operations go through this
# so the PreToolUse hook can confirm each command
tool(
'execute_command',
'Execute a shell command and return its output. Use this for ALL shell operations including listing files, reading files, etc. The user will be prompted to approve each command.',
{
type => 'object',
properties => {
command => {
type => 'string',
description => 'The shell command to execute (e.g., "ls -la", "cat file.txt", "find . -name *.pl")',
},
working_dir => {
type => 'string',
description => 'Directory to run command in (optional, defaults to current directory)',
},
},
required => ['command'],
},
sub {
my ($params, $loop) = @_;
return _execute_command($session, $params, $loop);
},
),
# get_working_directory tool - safe, no confirmation needed
tool(
'get_working_directory',
'Get the current working directory. This is safe and does not require user confirmation.',
{
type => 'object',
properties => {},
},
sub {
my ($params, $loop) = @_;
my $future = $loop->new_future;
$future->done(_mcp_result(getcwd()));
return $future;
},
),
# read_file tool - safe read operation, no confirmation needed
tool(
'read_file',
'Read the contents of a file. Safe operation - does not require user confirmation. Use this instead of execute_command for reading files.',
{
type => 'object',
properties => {
path => {
type => 'string',
description => 'Path to the file to read',
},
lines => {
type => 'integer',
description => 'Number of lines to read from the beginning (optional)',
},
tail => {
type => 'integer',
description => 'Number of lines to read from the end (optional)',
},
},
required => ['path'],
},
sub {
my ($params, $loop) = @_;
return _read_file_safe($session, $params, $loop);
},
),
# list_directory tool - safe read operation, no confirmation needed
tool(
'list_directory',
'List the contents of a directory. Safe operation - does not require user confirmation. Use this instead of execute_command for listing files.',
{
type => 'object',
properties => {
path => {
type => 'string',
description => 'Path to the directory to list (defaults to current directory)',
},
pattern => {
type => 'string',
description => 'Glob pattern to filter files (e.g., "*.pl", "*.txt")',
},
long_format => {
type => 'boolean',
description => 'Show detailed file information (size, date, permissions)',
},
show_hidden => {
type => 'boolean',
description => 'Include hidden files (starting with .)',
},
},
},
sub {
my ($params, $loop) = @_;
return _list_directory_safe($session, $params, $loop);
},
),
# search_files tool - safe search operation, no confirmation needed
tool(
'search_files',
'Search for files by name pattern or content. Safe operation - does not require user confirmation.',
{
type => 'object',
properties => {
pattern => {
type => 'string',
description => 'File name pattern to search for (e.g., "*.pm", "config*")',
},
content => {
type => 'string',
description => 'Text pattern to search for within files (grep)',
},
path => {
type => 'string',
description => 'Directory to search in (defaults to current directory)',
},
max_depth => {
type => 'integer',
description => 'Maximum directory depth to search',
},
},
},
sub {
my ($params, $loop) = @_;
return _search_files_safe($session, $params, $loop);
},
),
# get_system_info tool - safe system information, no confirmation needed
tool(
'get_system_info',
'Get system information including OS, disk space, and memory. Safe operation - does not require user confirmation.',
{
type => 'object',
properties => {
info_type => {
type => 'string',
description => 'Type of info: "all", "os", "disk", "memory", "processes" (defaults to "all")',
enum => ['all', 'os', 'disk', 'memory', 'processes'],
},
},
},
sub {
my ($params, $loop) = @_;
return _get_system_info($session, $params, $loop);
},
),
];
}
sub _execute_command {
my ($session, $params, $loop) = @_;
my $command = $params->{command};
my $dir = $params->{working_dir} // $session->working_dir;
my $colorful = $session->colorful;
# Stop spinner before prompting for approval
if ($session->can('_spinner') && $session->_spinner) {
stop_spinner($session->_spinner);
$session->_spinner(undef);
}
# Prompt for approval before executing
my ($approved, $new_command) = _confirm_command($session, $command);
unless ($approved) {
my $future = $loop->new_future;
$future->done(_mcp_result("User cancelled command", 1));
return $future;
}
# Use potentially edited command
$command = $new_command if defined $new_command;
# Start execution spinner
if ($colorful) {
$session->_spinner(start_spinner("Executing...", $loop));
}
# Record in history
push @{$session->_history}, {
time => _timestamp(),
command => $command,
status => 'running',
};
my $future = $loop->new_future;
my $stdout = '';
my $stderr = '';
my $process = IO::Async::Process->new(
command => [ '/bin/sh', '-c', $command ],
($dir && -d $dir ? (setup => [ chdir => $dir ]) : ()),
stdout => {
into => \$stdout,
},
stderr => {
( run in 1.383 second using v1.01-cache-2.11-cpan-140bd7fdf52 )