NAME
Langertha::Raider::Permissions - Internal permission policy of a raider session: mode, rules with sources, session rules
VERSION
version 0.504
SYNOPSIS
# Internal to Langertha-Raider -- no API promise.
my $policy = Langertha::Raider::Permissions->new(
root => $root,
mode => 'default',
rules => {
allow => [ { rule => 'Bash(npm run test:*)', source => '.claude/settings.json' } ],
deny => [ { rule => 'Read(./.env)', source => '.raider/config.yml' } ],
},
additional_directories => [ { path => '../docs', source => '.claude/settings.json' } ],
);
my $verdict = $policy->decide( { name => 'bash', source => 'engine:2',
arguments => { command => 'npm run test' } } );
# { verdict => 'allow', by => 'rule', rule => 'Bash(npm run test:*)', source => ..., reason => ... }
$policy->add_session_rule('Bash(make)');
$policy->set_mode('acceptEdits');
my $child = $policy->for_subrun; # same mode and rules, shared session rules
DESCRIPTION
Internal module. Its interface may change without notice.
The permission policy of one raider session (ADR 0021, building on the gate and the approval of ADR 0005): a mode, the allow / ask / deny rules in Claude Code syntax (Langertha::Raider::Permissions::Rule) with the source of each, the rules the person allowed for this session, and the additional directories. "decide" answers the canonical call the tool gate of Langertha::Raider sees with allow, ask or deny. The policy only decides -- asking the person, recording events and building approvals is the application's part.
Order of a decision: a matching deny rule (from any source) refuses; in bypassPermissions everything else is allowed; in plan a tool that changes something (write, code or message effects) is refused; a matching ask rule asks; a matching allow rule (or session rule) allows; without a rule the mode decides:
default-- tools that only read, or have no effect outside the run, and the person-facing control tools (ask_user,raider_ask_user,todo_write,skill,exit_plan_mode) are allowed; everything that writes, runs code, talks to the network or sends messages asks, and so does every tool raider does not know (a foreign MCP tool).enter_plan_modeasks too, in every mode butplanandbypassPermissions: the person agrees to plan mode, as in Claude Code (ADR 0025).acceptEdits-- asdefault, andwrite_file/edit_file/multi_editare allowed inside the root or an additional directory, and so areenter_worktreeandexit_worktree(ADR 0032 §4).auto-- asacceptEdits, and bash commands that only read are allowed without asking: a fixed list (Langertha::Raider::Permissions::ReadOnlyBash) where every part of the command must be on it and none may hold a substitution or a redirection into a file. The raider equivalent of Claude Code'sdefaultMode: "auto"; everything else asks, deny and ask rules hold, the person's allow rules add to it.plan-- asdefaultfor reading and network tools; changing tools are refused, even when an allow rule names them.bypassPermissions-- everything is allowed except what a deny rule refuses.
Effects come from Langertha::Raider::ToolEffects, and only for raider's own tools: a tool from a catalog:NAME source, or from a source named in "source_servers", is foreign, whatever its name.
root
The project root. Required. Relative rule paths and relative additional directories are read against it, and so are relative path arguments, as the file tools do. The application's policy follows its working root ("move_to").
home
The home for ~/ in rules and directories. Defaults to "home_dir" in Langertha::Raider::Home.
mode
The permission mode: default, acceptEdits, auto, plan or bypassPermissions. Croaks on construction with any other name; changed with "set_mode".
mode_source
Where the mode came from (--permission-mode, a settings file, ...), for reports.
rules
The configured rules as { allow => [ { rule, source } ], ask => [...], deny => [...] } -- the shape of "permissions" in Langertha::Raider::Context::Settings. Every list keeps its order; sources are mixed freely.
additional_directories
[ { path, source } ]: directories outside the root the file tools may use. ~/x is under the home, a relative path under the root.
source_servers
{ 'engine:N' => 'server' }: tool sources that are foreign MCP servers, with the name mcp__server rules use. A catalog:NAME source is always the foreign server NAME; every other source (engine:N, inline, raider) is raider's own unless named here.
session_rules
The rules allowed for this session ("add_session_rule"), as Langertha::Raider::Permissions::Rule objects. The array is shared with every policy "for_subrun" derives, so a rule allowed in a sub-run holds in the session and the other way round. Lives in memory only.
run_rules
The allow rules of the run in progress ("add_run_rules"): the allowed-tools of a /name run or of an entry the model loaded with the skill tool (ADR 0030 §3), as Langertha::Raider::Permissions::Rule objects with their source (/NAME allowed-tools). Shared with every policy "for_subrun" derives, like "session_rules", and emptied in place by "clear_run_rules" when the run ends -- unless a policy took its own copy ("own_run_rules").
ask_outside_roots
False by default: the file and search tools are confined to the root and the "additional_directories" and refuse every other path themselves (the raider layout). True in the Claude layout (ADR 0023), where those tools are not confined: a call of read_file, list_files, write_file, edit_file, multi_edit, glob or grep whose path -- as written or as real path -- leaves the allowed roots is then not allowed by the mode in default, acceptEdits, auto and plan: it asks, unless an allow rule (Read(//tmp/**), Edit(~/notes/**)) covers it. Deny rules and plan refuse first; bypassPermissions allows it, as Claude Code does.
worktree_isolation
{ top => '/repo', worktree => '/repo/.claude/worktrees/agent-1' } when the run works in a worktree (ADR 0032 §3): the main checkout's top and the worktree's. Then "main_checkout_refusal" refuses edits into the main checkout. Unset by default.
ignored_modes
[ { value, source, reason } ]: mode names from files that are not modes, skipped by "from_sources". For reports.
from_sources
my $policy = Langertha::Raider::Permissions->from_sources(
root => $root,
cli_mode => { value => 'plan', source => '--permission-mode' }, # optional
sources => [ $config->permissions, $settings->permissions ],
);
The policy of a session from its sources, each in the shape of "permissions" in Langertha::Raider::Context::Settings. The rules and additional directories of all sources are concatenated. The mode is the first of cli_mode and the sources' default_mode in the order given that is a mode; a default_mode that is none is skipped and kept in "ignored_modes". Without any, default. session_rules, run_rules, source_servers, ask_outside_roots and worktree_isolation are passed on as they are (a policy read again keeps the session's rules). Croaks on an invalid cli_mode -- the command line refuses it before.
report
The policy for config explain and /permissions: mode, mode_source, mode_note (only in auto mode: how it maps), ignored_modes, rules (per kind the valid rules with their source, session rules included), invalid_rules (with kind and error) and additional_directories (absolute, with their source).
mode_note
How the auto mode maps, for config explain and /permissions: for a mode from the Claude settings claude auto - raider acceptEdits + read-only bash allowlist>. Nothing in any other mode.
modes
The mode names, in display order. Callable on the class.
is_mode
Langertha::Raider::Permissions->is_mode('acceptEdits'); # 1
True for a valid mode name (exactly as Claude Code writes it).
rule_list
Every valid rule: the configured ones (deny, ask, allow), then the session rules, then the run rules.
invalid_rules
The configured rules that could not be parsed or name an unknown tool; they are ignored.
add_session_rule
my $rule = $policy->add_session_rule('Bash(make test)');
Allows what the rule names for the rest of the session (source session). Croaks on an invalid rule. Returns the rule; adding the same text again changes nothing.
add_run_rules
my @added = $policy->add_run_rules( [ 'Bash(git status:*)', 'Read' ], '/review allowed-tools' );
Allows what the rules name for the run in progress only ("run_rules"), with $source as their source (default run). Deny and ask rules, and plan mode, still win. A text that is no valid rule is skipped, as an invalid rule never widens anything; one already there with the same source is not added twice. Returns the rules that were added.
clear_run_rules
$policy->clear_run_rules; # all of them: the run ended
$policy->clear_run_rules(@added); # only these
Takes run rules out again, in place, so every policy sharing them ("for_subrun") loses them too.
set_mode
my $from = $policy->set_mode( 'default', 'exit_plan_mode' );
Switches the mode; $source (default session) becomes "mode_source". Returns the previous mode. Croaks on an unknown mode.
additional_directory_paths
The additional directories as absolute paths (.. taken out).
allowed_roots
The root followed by the additional directories, absolute: where the file tools may work.
policy_revision
The SHA-256 (hex) over the mode, every rule with its source, the session rules, the run rules and the additional directories. An approval (Langertha::Raider::Approval) is bound to it: switching the mode or allowing a session rule invalidates approvals given before.
for_subrun
my $policy_of_subagent = $policy->for_subrun;
The policy a sub-run (a sub-agent, a delegated task) uses: a new object with the mode this policy has now, the same configured rules, additional directories and source servers, and the same session rules and run rules arrays. A mode change of either afterwards stays with that one (a sub-agent leaving plan mode does not take the session with it); a session rule allowed in either holds in both. A sub-run never gets more than its parent had: there is no argument to widen it.
my $isolated = $policy->for_subrun(
root => '/repo/.claude/worktrees/agent-1',
worktree_isolation => { top => '/repo', worktree => '/repo/.claude/worktrees/agent-1' },
);
A subagent in a worktree of its own (ADR 0032 §2) moves the policy: root replaces the root (the rules and additional directories mean the same files in the copy), worktree_isolation sets "worktree_isolation". Without them the sub-run keeps the parent's -- a subagent of an isolated one works in the same worktree.
own_run_rules
$policy_of_subagent->own_run_rules;
Takes this policy's own copy of the "run_rules" it shares, as they are now: rules a later run adds do not reach it, and the end of the run that had them does not take them away. A background subagent does this when it starts (ADR 0027): it outlives the run it was started in.
move_to
$policy->move_to(
root => '/repo/.claude/worktrees/session-1',
worktree_isolation => { top => '/repo', worktree => '/repo/.claude/worktrees/session-1' },
);
$policy->move_to( root => '/repo' ); # back: no isolation
Moves this policy to another root in place -- the application's policy follows its working root (ADR 0032 §3): "root" becomes root, "worktree_isolation" becomes worktree_isolation or is cleared without it. Mode, rules, session and run rules stay; so do the approvals bound to "policy_revision", which the root is no part of. Policies a "for_subrun" made before keep their root.
is_builtin_call
my $ours = $policy->is_builtin_call( { name => 'enter_worktree', source => 'engine:3' } );
True when the call's tool is raider's own: its source is no catalog:NAME and none of the "source_servers".
decide
my $verdict = $policy->decide( { name => $tool, source => $source, arguments => \%args } );
The verdict on the canonical call: a hash with verdict (allow, ask, deny), by (rule or mode), reason (a sentence for the person or the model), mode, and for a rule rule (its text) and source; for the mode source is "mode_source". A command of several parts that several allow rules cover together names all of them. The shape is the one the tool gate of Langertha::Raider takes.
With "ask_outside_roots" a call of a file or search tool whose path leaves the root and the additional directories asks (after deny, plan, ask and allow rules, before the mode allows a read or an edit); skip_roots => 1 as a further argument leaves that check out ("hides_read" does).
rule_matches
my $yes = $policy->rule_matches( 'Bash(git *)', $call );
Whether one rule in Claude Code syntax applies to the canonical call -- the if field of a Claude Code hook (ADR 0022). It matches the way a deny or ask rule does (any part of a bash command, the written or the real path), and is not added to the policy. undef for a rule that cannot be parsed: the caller decides what that means (a hook then runs, as in Claude Code when it cannot tell).
outside_paths
my @outside = $policy->outside_paths($call);
For a call of a file or search tool (read_file, list_files, write_file, edit_file, multi_edit, glob, grep): the absolute forms of its path -- as written, and as real path with symbolic links followed -- that lie outside the root and the additional directories. Empty when the path stays inside (or the call is of another tool). A symbolic link inside the root that points out of it is outside.
worktree_class
The class that lists a repository's worktrees for "main_checkout_refusal": Langertha::Raider::Worktree.
main_checkout_refusal
my $text = $policy->main_checkout_refusal($call); # or nothing
With "worktree_isolation": for a write_file, edit_file or multi_edit whose path (relative to the "root", real path followed) is inside the main checkout's top but outside every worktree of the repository (git worktree list), the refusal of ADR 0032 §3:
PATH is in the main checkout TOP; this session works in the worktree W. Edit W/REL instead.
Nothing for every other call. The application's gate asks it before any rule or mode ("tool_gate" in Langertha::Raider::Application).
read_rules_active
True when a deny or ask rule applies to reading files (Read(...), read_file): only then can "hides_read" hide anything.
hides_read
next if $policy->hides_read('/abs/path/secrets/key');
True when reading the file at the absolute $path would not simply be allowed: a deny rule refuses it or an ask rule asks. glob and grep leave such files out of their results ("hider" in Langertha::Raider::SearchTools) instead of refusing the whole search.
suggest_rule
my $text = $policy->suggest_rule($call); # 'Bash(make test)'
The rule the person would allow with "yes, for this session": bash -> Bash(EXACT COMMAND); write_file / edit_file / multi_edit -> Edit(DIR/**) for the directory of the file (Edit(/**) in the root, Edit(//ABS/**) outside it); a reading file or search tool whose path leaves the root and the additional directories ("outside_paths") -> Read(//DIR/**) for the directory of the file, or the directory searched; web_fetch -> WebFetch(domain:HOST); a foreign tool -> mcp__SERVER__TOOL (a tool of a configured server, already named so, keeps its name); any other tool -> its name. Returns nothing when no rule can name the call safely: a command with substitutions, redirections into files or heredocs, or a foreign tool whose server has no name.
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.