NAME
Langertha::Raider::Context::Settings - Internal reader of the Claude Code settings files (user, project, local)
VERSION
version 0.504
SYNOPSIS
# Internal to Langertha-Raider -- no API promise.
my $settings = Langertha::Raider::Context::Settings->new(
project_dir => $root,
);
my $perms = $settings->permissions; # allow/ask/deny with sources
my %env = %{ $settings->env_values }; # KEY => value
my $enabled = $settings->enabled_plugin_map; # 'name@market' => 0|1
warn $_->{label}.': '.$_->{error}."\n" for grep { $_->{error} } @{ $settings->files };
DESCRIPTION
Internal module. Its interface may change without notice.
The Claude Code settings of a project as one value (ADR 0019 §3). Three files are read, later ones winning:
- 1. settings.json in "claude_home" -- scope
user, label~/.claude/settings.json(or the absolute path when "claude_home" is not under the home directory) - 2. .claude/settings.json in "project_dir" -- scope
project - 3. .claude/settings.local.json in "project_dir" -- scope
local
A missing file is no error. A file that cannot be read, is not valid JSON or is not a JSON object is recorded in "files" and contributes nothing; it is never fatal. Every entry keeps the label of the file it came from as source.
Raider uses permissions, hooks, disableAllHooks, disableSkillShellExecution, env, enabledPlugins and the approvals of project MCP servers (enableAllProjectMcpServers, enabledMcpjsonServers, disabledMcpjsonServers; ADR 0029) and worktree.baseRef (ADR 0032). Every other top-level key is listed by "ignored". Here they are only read; evaluating permissions (stream P), running hooks (stream H) and applying the environment are the caller's business.
project_dir
The project directory whose .claude/ holds the project and local settings. Required.
claude_home
The user's Claude Code directory. Defaults to "claude_home" in Langertha::Raider::Home: $ENV{CLAUDE_CONFIG_DIR} when that is set and not empty, else .claude in the home directory. undef when there is no home at all: then no user file is read.
files
for my $f (@{ $settings->files }) {
printf "%s %s%s\n", $f->{label}, $f->{exists} ? 'found' : 'absent',
$f->{error} ? ' ('.$f->{error}.')' : '';
}
The three settings files in read order, each a new hash ref:
{
file => '/abs/path/.claude/settings.json',
label => '.claude/settings.json',
scope => 'user' | 'project' | 'local',
exists => 0 | 1,
error => undef | 'invalid JSON: ...' | 'not a JSON object' | 'unreadable: ...',
}
Without a "claude_home" the user file is not in the list.
permissions
my $p = $settings->permissions;
for my $rule (@{ $p->{deny} }) { ... $rule->{rule}, $rule->{source} ... }
The permissions of all files, the input of the permission engine:
{
allow => [ { rule => 'Bash(npm run test:*)', source => '.claude/settings.json' }, ... ],
ask => [ { rule => ..., source => ... }, ... ],
deny => [ { rule => ..., source => ... }, ... ],
default_mode => { value => 'acceptEdits', source => ... } | undef,
additional_directories => [ { path => '../docs', source => ... }, ... ],
}
allow, ask, deny and additional_directories are the lists of every file concatenated in read order (user first), each entry as written -- nothing is deduplicated, resolved or validated. Entries that are not plain strings (or are empty) are skipped. default_mode is the defaultMode of the last file that sets one as a string.
hooks
my $groups = $settings->hooks->{PreToolUse} // [];
for my $g (@$groups) { ... $g->{matcher}, $g->{hooks}, $g->{source} ... }
The hooks of all files, only stored (stream H runs them):
{
PreToolUse => [
{
matcher => 'Bash', # undef when the group has none
hooks => [ { type => 'command', command => '...' }, ... ], # as in the file
source => '.claude/settings.json',
},
...
],
...
}
Per event name the matcher groups of every file, concatenated in read order. A group that is not an object is skipped; a matcher that is not a string is undef; hooks is the file's list untouched (an empty list when it is missing or not a list).
disable_all_hooks
return if $settings->disable_all_hooks;
1 when the disableAllHooks of the last file that sets it as a JSON boolean is true, else 0. As in Claude Code, a later file wins, so a project's false overrides the user's true.
skip_dangerous_mode_prompt
$settings->skip_dangerous_mode_prompt;
1 when the user file (~/.claude/settings.json, no other) sets skipDangerousModePermissionPrompt to a JSON true, else 0. Claude Code honours it only from user and managed settings; with it a session started in bypassPermissions loads project MCP servers without asking (ADR 0029 Addendum 1).
disable_skill_shell_execution
disable_shell => $settings->disable_skill_shell_execution,
1 when the disableSkillShellExecution of the last file that sets it as a JSON boolean is true, else 0, a later file winning: the !`cmd` of skills and commands is then not run (ADR 0030 §2, "disable_shell" in Langertha::Raider::SlashCommand).
env
my $e = $settings->env; # { KEY => { value => '1', source => '.claude/settings.json' } }
The env of all files merged per key, a later file winning. Only string and number values count (numbers as strings); anything else, booleans included, is skipped.
env_values
local @ENV{ keys %$values } = values %$values;
"env" without the sources: { KEY => value }.
enabled_plugins
my $ep = $settings->enabled_plugins;
# { 'superpowers@claude-plugins-official' => { value => 1, source => '~/.claude/settings.json' } }
The enabledPlugins of all files merged per key, a later file winning. Only JSON booleans count; value is 1 or 0.
enabled_plugin_map
"enabled_plugins" without the sources: { 'name@market' => 0|1 }, the enabled input of Langertha::Raider::Context::Plugins.
mcpjson_approvals
my $a = $settings->mcpjson_approvals;
for my $d (@{ $a->{disabled} }) { ... $d->{name}, $d->{source}, $d->{scope} ... }
The approvals of project MCP servers (.mcp.json, ADR 0029 §7) of every file, in read order, each entry with the label and the scope (user, project, local) of its file. Which of them count depends on the workspace trust; Langertha::Raider::MCP::Config decides that.
{
enable_all => [ { value => 1, source => '.claude/settings.local.json', scope => 'local' }, ... ],
enabled => [ { name => 'github', source => ..., scope => ... }, ... ],
disabled => [ { name => 'db', source => ..., scope => ... }, ... ],
}
enable_all holds one entry per file whose enableAllProjectMcpServers is a JSON boolean (value 1 or 0); enabled and disabled are the enabledMcpjsonServers and disabledMcpjsonServers lists concatenated. Entries that are not non-empty strings are skipped.
worktree_base_ref
my $base = $settings->worktree_base_ref; # { value => 'head', source => '.claude/settings.json' } or undef
worktree.baseRef (ADR 0032 §2), a later file winning: where a new worktree branches from (fresh or head, as written -- the caller decides what another value means). Undef when no file sets a string.
ignored
for my $i (@{ $settings->ignored }) {
say $i->{key}.' ('.$i->{source}.'): '.$i->{reason};
}
Every top-level key raider does not use (model, statusLine, extraKnownMarketplaces, ...), per file in read order and sorted by key inside a file, each as { key, source, reason => 'not used by raider' }.
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.