NAME
Langertha::Raider::SlashCommand - Internal renderer of a skill or command body for /name and the skill tool
VERSION
version 0.504
SYNOPSIS
# Internal to Langertha-Raider -- no API promise.
my $renderer = Langertha::Raider::SlashCommand->new(
root => $app->root,
permissions => $app->permissions,
validator => $app->loadguard->validator,
session_id => $session ? $session->id : undef,
disable_shell => $settings_say_disableSkillShellExecution,
);
my $r = $renderer->render(
entry => $catalog->find('review'),
body => $catalog->body('review'),
arguments => 'src/App.pm "be strict"',
attach_files => 1, # a /name run, not the skill tool
);
if ( defined $r->{error} ) { say $r->{error}; return }
$renderer->record_reads( $r, $app->file_read_state ); # after begin_run
send_to_model( $r->{text} );
DESCRIPTION
Internal module. Its interface may change without notice.
One renderer for the body of a catalogue entry (a skill or a command, Langertha::Raider::Context::Skills), used by /name and by the skill tool (ADR 0030 §2). "render" runs three steps, in this order:
- 1. Arguments. The argument string is split shell-style (quotes group, a backslash escapes; Text::ParseWords, and on unbalanced quotes at whitespace). In one pass over the body -- an inserted value is never expanded again:
-
$ARGUMENTS the whole argument string ('' without arguments) $ARGUMENTS[N], $N the N-th argument, 0-based; stays as typed without one $name a name from the frontmatter "arguments" (a list, or a space-separated string) for its position; '' without one ${CLAUDE_SKILL_DIR} the entry's dir ${CLAUDE_PROJECT_DIR} the root ${CLAUDE_SESSION_ID} the session id ('' without a session) ${CLAUDE_PLUGIN_ROOT} the entry's plugin_root (stays as typed for an entry that is not from a plugin) \$ a literal $With arguments given and no argument placeholder (
$ARGUMENTS,$ARGUMENTS[N],$N,$name) in the body,ARGUMENTS: <args>is appended after a blank line. A placeholder whose index has no argument still counts as used. - 2. Shell injection.
!`cmd`at the start of a line or after whitespace, and fenced blocks opened by a line```!and closed by a line```, are commands. Every command is first held against the permission policy as raider'sbashcall ("permissions", ADR 0021), with the entry'sallowed-tools("allowed_tool_rules") added as run rules ("add_run_rules" in Langertha::Raider::Permissions, source "grant_source") while the commands are checked and taken out again after -- deny and ask rules still win. A command may run only onallow;denyoraskaborts the whole render with the reason -- nobody is asked while a command renders. All commands are checked before the first one runs, so a refusal leaves no side effect. Then the loadguard gate ("validator", ADR 0018) is asked and the command runs withbash -cin the root ("run_command"). Its output (standard output and standard error together, trailing newlines taken off) replaces the placeholder and is not scanned again -- neither for commands nor for@file. A non-zero exit aborts, except exit 1 of a search or compare command (grep,egrep,fgrep,rg,diff,cmp,test,[); so does a command still running after "timeout" seconds. With "disable_shell" each command is replaced by[shell command execution disabled by policy]and nothing is checked or run. - 3. File references (only with
attach_files => 1, a/namerun). Each@path(or@"path with spaces") at the start of a line or after whitespace, in the text of the body and the arguments -- not in command output -- that names an existing file or directory inside the root or an additional directory ("extra_roots"), and that no deny or ask rule for reading hides ("hides_read" in Langertha::Raider::Permissions), is attached after the text. Punctuation that ends a sentence (.,;:!?)) is not part of the path when the path without it exists. A file is read the wayread_filereturns it (Langertha::Raider::FileTools, the same limits and texts; images and PDFs are not attached, as the result is text), a directory is listed the waylist_fileslists it. A reference that names nothing, lies outside, or that the tool refuses stays plain text. Each attachment is shaped after Claude Code's, with raider's tool names: -
<system-reminder> Called the read_file tool with the following input: {"path":"/abs/real/path"} </system-reminder> <system-reminder> Result of calling the read_file tool: 1 first line ... </system-reminder>The files attached count as read for read-before-write (ADR 0023): "record_reads" puts them into a run's Langertha::Raider::FileTools::ReadState.
A skill's text (kind not command) starts with Base directory for this skill: DIR and a blank line, as the skill tool has always answered (ADR 0026); a command's text has no such line.
root
The project root. Required. Commands run in it, @path is relative to it, ${CLAUDE_PROJECT_DIR} is it.
permissions
The permission policy of the session (Langertha::Raider::Permissions). Without it no shell command passes: a body with one aborts. Its deny and ask rules for reading also hide files from @path.
validator
Optional. The loadguard gate as the bash tool gets it ("validator" in Langertha::Raider::Loadguard): called as $validator->($command, $root); 1 lets the command run, anything else aborts with Command REASON.
extra_roots
The directories besides the root that @path may reach: an ArrayRef, or a CodeRef returning the list, asked on every render. Defaults to the additional directories of "permissions".
session_id
The session's id for ${CLAUDE_SESSION_ID}; '' without one.
disable_shell
True (the Claude Code setting disableSkillShellExecution): no command runs, each is replaced by "disabled_text".
timeout
Seconds a command may run before the render aborts: 120, the bash tool's default.
kill_grace
Seconds between SIGTERM and SIGKILL to the process group of a command that timed out.
runner
Optional. Replaces how a command is run (tests): called as $runner->($command, $root, $timeout), it returns what "run_command" returns. The permission check and the loadguard gate still come first.
disabled_text
[shell command execution disabled by policy], what a command becomes with "disable_shell".
exit_one_commands
grep egrep fgrep rg diff cmp test [: the commands whose exit 1 does not abort the render.
split_arguments
my @args = $renderer->split_arguments('a "b c" d\ e'); # a, b c, d e
The argument string split shell-style: quotes group, a backslash escapes. With unbalanced quotes it is split at whitespace instead.
argument_names
my @names = $renderer->argument_names($entry);
The names of the frontmatter arguments (a list, or a space-separated string), in position order. Names that are not identifiers are left out -- their position still counts.
expand_arguments
my ( $text, $used ) = $renderer->expand_arguments( $entry, $body, $args );
Step 1 alone: the body with every placeholder replaced (see "DESCRIPTION"), and whether an argument placeholder was in it. Does not append ARGUMENTS:; "render" does.
allowed_tool_rules
my @rules = $renderer->allowed_tool_rules($entry);
# 'Bash(git status:*)', 'Read', 'mcp__github'
The entry's frontmatter allowed-tools (also allowed_tools) as permission rule texts in the syntax of ADR 0021 -- Claude Code's, which Langertha::Raider::Permissions::Rule reads, translating Claude Code tool names through Langertha::Raider::ToolNames. The value is a string with the rules separated by commas or whitespace outside parentheses, or a YAML list of such strings. A text that is not a valid rule is left out, as an invalid rule never widens anything. Called with a plain value instead of an entry, it parses that value.
grant_source
my $source = $renderer->grant_source($entry); # '/review allowed-tools'
The source the entry's allowed-tools carry as run rules ("add_run_rules" in Langertha::Raider::Permissions).
check_command
my $verdict = $renderer->check_command('git status');
The verdict of the policy ("decide" in Langertha::Raider::Permissions) on raider's bash call with the command, as it stands -- with the run rules it holds ("run_rules" in Langertha::Raider::Permissions). Without "permissions" the verdict is deny. "render" adds the entry's allowed-tools as run rules while it checks.
run_command
my $ran = $renderer->run_command('git status --short');
# { exit_code => 0, output => "M lib/App.pm" } or { error => 'timed out after 120 seconds' }
Runs the command with bash -c in the root, in a process group of its own, standard input from /dev/null, standard output and standard error into one file; returns its exit code and output (decoded as UTF-8, trailing newlines taken off), or error when it could not start or ran longer than "timeout" seconds (its process group is stopped then). With "runner" the runner does it.
render
my $r = $renderer->render(
entry => $entry, # a catalogue hash: name, dir, file, source, frontmatter, kind, plugin_root
body => $body, # its text without frontmatter
arguments => $string, # optional
attach_files => 1, # optional: step 3, for a /name run
);
Renders the body (see "DESCRIPTION"). Returns a hash:
{
text => '...', # on success -- what goes to the model
error => '...', # instead of text when the render aborts
arguments => [ ... ], # the split arguments
commands => [ { command, verdict, exit_code?, reason? }, ... ],
files_read => [ { path => '/real/path', kind => 'file' | 'directory',
bytes => $raw | partial => 1 }, ... ],
}
commands lists every command found, in order, with its verdict (allow, deny, ask, disabled, refused by the loadguard gate), for tracing; files_read the attachments ("record_reads"). An abort sends nothing to the model; its error names the command and the reason. Croaks without entry.
record_reads
$renderer->record_reads( $result, $app->file_read_state );
Records the files of files_read in the read state of a run (Langertha::Raider::FileTools::ReadState): a file attached whole with its bytes, a part of one as partial. Directories are not recorded. Call it once the run has begun -- "begin_run" in Langertha::Raider::Application empties the read state.
SEE ALSO
SUPPORT
Issues
Please report bugs and feature requests at https://github.com/Getty/langertha-raider/issues.
IRC
Join #langertha on irc.perl.org or message Getty directly.
CONTRIBUTING
Contributions are welcome! Please fork the repository and submit a pull request.
AUTHOR
Torsten Raudssus <torsten@raudssus.de> https://raudssus.de/
COPYRIGHT AND LICENSE
This software is copyright (c) 2026 by Torsten Raudssus.
This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself.