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's bash call ("permissions", ADR 0021), with the entry's allowed-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 on allow; deny or ask aborts 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 with bash -c in 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 /name run). 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 way read_file returns 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 way list_files lists 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.

https://raudssus.de/