Claude-Agent
view release on metacpan or search on metacpan
lib/Claude/Agent/Hook/Matcher.pm view on Meta::CPAN
package Claude::Agent::Hook::Matcher;
use 5.020;
use strict;
use warnings;
use Claude::Agent::Logger '$log';
use Scalar::Util qw(blessed);
use Try::Tiny;
use Future;
use Types::Common -types;
use Marlin
'matcher', # Regex pattern for tool names (optional)
'hooks' => sub { [] }, # ArrayRef of coderefs
'timeout' => sub { 60 }; # Timeout in seconds
=head1 NAME
Claude::Agent::Hook::Matcher - Hook matcher for Claude Agent SDK
=head1 DESCRIPTION
Defines a hook matcher that triggers callbacks for specific tools.
=head2 ATTRIBUTES
=over 4
=item * matcher - Optional regex pattern to match tool names
=item * hooks - ArrayRef of callback coderefs
=item * timeout - Timeout in seconds (default: 60)
=back
=head2 CALLBACK SIGNATURE
Hooks receive input data, tool use ID, context, and an optional IO::Async::Loop.
They can return either a hashref (synchronous) or a Future (asynchronous).
# Synchronous hook (backward compatible)
sub callback {
my ($input_data, $tool_use_id, $context, $loop) = @_;
# $input_data contains:
# - tool_name: Name of the tool
# - tool_input: Input parameters for the tool
# $context contains:
# - session_id: Current session ID
# - cwd: Current working directory
# $loop is the IO::Async::Loop (optional, may be undef)
# Return hashref with decision:
return {
decision => 'continue', # or 'allow', 'deny'
reason => 'Optional reason',
# For 'allow', can include:
updated_input => { ... },
};
}
# Asynchronous hook (returns Future)
sub async_callback {
my ($input_data, $tool_use_id, $context, $loop) = @_;
# Use loop for async operations (e.g., HTTP requests)
return $loop->delay_future(after => 0.1)->then(sub {
# Perform async validation...
return Future->done({
decision => 'allow',
});
});
}
=head2 METHODS
=head3 matches
my $bool = $matcher->matches($tool_name);
Check if this matcher matches the given tool name.
B<IMPORTANT - Platform Limitation:> Regex timeout protection uses alarm()
which only works on Unix-like systems. On Windows (MSWin32, cygwin), a
post-execution time check is performed, but B<this cannot interrupt a
regex that hangs indefinitely> - it only detects slow patterns after
completion. Pattern length is limited to 1000 characters and basic nested
quantifier detection is performed to provide additional ReDoS protection,
but sophisticated ReDoS attacks with shorter patterns may still be possible.
For security-critical applications, especially on Windows, consider using
re::engine::PCRE2 or Regexp::Timeout for proper cross-platform timeout
support, or use pre-validated patterns only.
=cut
sub matches {
my ($self, $tool_name) = @_;
# Handle undefined tool name
return 0 unless defined $tool_name;
# No matcher means match all
return 1 unless defined $self->matcher;
my $pattern = $self->matcher;
# If it's a simple string (no regex metacharacters), do exact match
# Use quotemeta to reliably detect plain strings vs regex patterns
if ($pattern eq quotemeta($pattern)) {
return $tool_name eq $pattern;
}
# Otherwise treat as regex with timeout protection against ReDoS
# Use Try::Tiny with finally to ensure alarm is always cleared
my $result;
try {
# Validate pattern length to mitigate ReDoS
if (length($pattern) > 1000) {
die "Pattern too long\n";
}
# Detect potentially dangerous ReDoS patterns (works on all platforms)
# Look for nested quantifiers like (a+)+ or (a*)*
if ($pattern =~ /\([^)]*[+*][^)]*\)[+*]/ ||
$pattern =~ /\([^)]*\|[^)]*\)[+*]/) {
die "Potentially dangerous nested quantifier pattern\n";
}
# Cross-platform timeout mechanism using Time::HiRes
# Note: alarm() only works on Unix-like systems (skipped on Windows)
# For true cross-platform ReDoS protection, consider re::engine::PCRE2
# or Regexp::Timeout. This implementation provides best-effort protection.
my $use_alarm = $^O ne 'MSWin32' && $^O ne 'cygwin';
my $timeout_seconds = 1;
if ($use_alarm) {
local $SIG{ALRM} = sub { die "Regex timeout\n" };
alarm($timeout_seconds);
}
# For Windows, use Time::HiRes-based polling timeout as fallback
# This is not as precise as alarm() but provides some protection
require Time::HiRes;
my $start_time = Time::HiRes::time();
my $compiled = qr/$pattern/;
$result = $tool_name =~ $compiled ? 1 : 0;
# Check if we exceeded timeout on Windows (post-facto detection)
if (!$use_alarm && (Time::HiRes::time() - $start_time) > $timeout_seconds) {
die "Regex timeout (Windows)\n";
}
alarm(0) if $use_alarm;
} catch {
$result = 0;
} finally {
alarm(0) if $^O ne 'MSWin32' && $^O ne 'cygwin';
};
return $result // 0;
}
=head3 run_hooks
my $future = $matcher->run_hooks($input_data, $tool_use_id, $context, $loop);
Run all hooks and return a Future that resolves to an arrayref of results.
Hooks may return either a hashref (synchronous) or a Future (asynchronous).
=cut
sub run_hooks {
my ($self, $input_data, $tool_use_id, $context, $loop) = @_;
( run in 2.205 seconds using v1.01-cache-2.11-cpan-97f6503c9c8 )