Skip to content

fix(permissions): fail closed when a settings file cannot be read or parsed - #4287

Merged
KuSh merged 2 commits into
rtk-ai:developfrom
mrhard9090:fix/permissions-unreadable-settings
Oct 18, 2026
Merged

KuSh merged 2 commits into
rtk-ai:developfrom
mrhard9090:fix/permissions-unreadable-settings

Conversation

@mrhard9090

Copy link
Copy Markdown
Contributor

Summary

Refs #4282

  • A Claude Code settings file that exists but cannot be read or parsed no longer counts as "no rules". load_permission_rules now prints a warning for both cases (the read failure was silent before) and adds a catch-all Ask rule, so every command gets at least Ask, the verdict nothing relaxes. Deny rules from the other settings files still apply first.
  • Missing files are skipped as before, and so is a file that is empty or only whitespace.
  • The loop moved into load_permission_rules_from(paths) so it can be tested with real files.

What changes in practice: Ask and Default both exit 3 from rtk rewrite, but a delegate that owns approval (RTK_REWRITE_HOST=openclaw, #3909) relaxes Default to an allowed rewrite. With a broken settings.json, a command that file denies was rewritten and allowed there (exit 0); now it exits 3.

Not in this PR: the hooks still call rtk rewrite ... 2>/dev/null, so the warning is not shown in that path, and the Cursor / Gemini / Droid loaders (read_json) are unchanged.

Test plan

  • New unit tests: test_valid_settings_file_rules_apply, test_unparseable_settings_file_fails_closed_to_ask, test_unreadable_settings_file_fails_closed_to_ask, test_unreadable_settings_file_keeps_other_files_deny_rules, test_missing_and_empty_settings_files_add_no_rules.
  • rtk rewrite "git push origin main" on Linux (WSL Ubuntu 24.04), CLAUDE_CONFIG_DIR with {"permissions":{"deny":["Bash(git push *)"]}} (unreadable = a directory in place of settings.json, since the shell runs as root and chmod 000 does not block it):
settings.json develop this PR develop, openclaw this PR, openclaw
valid 2 2 2 2
unparseable (trailing comma) 3, warning 3, warning 0, warning 3, warning
unreadable 3, no warning 3, warning 0, no warning 3, warning
  • cargo test --bin rtk on Linux: 3787 passed, 8 ignored.
  • cargo fmt --all -- --check and cargo clippy --all-targets (clean) on Windows (MSVC).

Written with help from Claude Opus 5.5.

…parsed

A Claude Code settings file that exists but cannot be read or parsed adds a
catch-all Ask rule and a warning instead of being skipped. Refs rtk-ai#4282.
@rtk-wshm-sync-bot rtk-wshm-sync-bot Bot added bug Something isn't working permissions security labels Sep 26, 2026
@rtk-wshm-sync-bot

Copy link
Copy Markdown

wshm · Automated triage by AI

📊 Automated PR Analysis

🐛 Type bug-fix
🟡 Risk medium

Summary

Fixes rtk's permission rule loader so that a settings.json file which exists but fails to read or parse no longer counts as having no rules. Instead, it now prints a warning and injects a catch-all Ask rule, ensuring commands fail closed (deny/ask) rather than falling through to Default. The loader body was also extracted into a testable load_permission_rules_from(paths) function with accompanying unit tests.

Review Checklist

  • Tests present
  • Breaking change
  • Docs updated

Linked issues: #4282


Analyzed automatically by wshm · This is an automated analysis, not a human review.

… parse failure

The read-failure warning and the delegate exit code are checked end to end
through `rtk rewrite` with `RTK_REWRITE_HOST=openclaw`, for a settings file
that cannot be read and one that cannot be parsed. A unit test checks that
deny rules from the other settings files still apply after an unparseable
one, as they already did after an unreadable one. The new test helpers say
why they would panic.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@KuSh KuSh left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Round 1 — APPROVED, follow-ups filed

Claim: when a Claude Code settings file exists but cannot be read or parsed, load_permission_rules warns and adds a catch-all Ask, so a delegate that relaxes Default (OpenClaw) no longer turns that file's denied commands into allowed rewrites; deny rules from the other files still apply first.
Scope: accept (round 1, frozen)
Ran: rtk rewrite (no host and RTK_REWRITE_HOST=openclaw) and rtk hook claude, 15 settings-file states (valid, trailing comma, chmod 000, directory, empty, whitespace only, missing, non-UTF-8, BOM + valid JSON, null, // comment, dangling symlink, CLAUDE_CONFIG_DIR pointing at a file, project .claude that is a file, project settings.local.json at chmod 000) × 2 layouts (the file alone; the file next to a valid settings.local.json with deny: Bash(rm *) and allow: Bash(git status)) × 2 commands, so 180 rows per binary. Binaries: merge-base, develop e542dca0b, this head, and this head merged onto develop. Develop and merge-base give identical results. 38 rows change, all in states where a file exists but cannot be read or parsed, plus the two "not a directory" states below. Invariants: non-default targets (project-level and CLAUDE_CONFIG_DIR files), under the hook, split streams (warning on stderr, rewrite on stdout), every decision. Savings: no filter output changes.
Pins: 9 decisions, 9 mutated, 2 survived the suite: removing the read-failure warning, and stopping at an unparseable file so later files' deny rules are lost. Both are now killed by the fixup's tests.
Matrix: executed this round; pin check: executed this round
CI @e27b47d1c: all green (ubuntu, macos, windows). On the fixup head cd9dff3: all green (14 jobs, including macos, ubuntu and both windows targets).

Blocks merge (0)

Filed as follow-ups

  • #4651 A settings file that cannot exist, or that holds only a BOM, fails closed. This is in this PR's own classification, but fixing it changes exit codes, so it is not a fixup. Two cases: .claude/settings.json under a .claude that is a regular file, or under a CLAUDE_CONFIG_DIR that points at a file, gives ENOTDIR (Windows reports NotFound for the same layout); and a settings.local.json holding only EF BB BF is not seen as empty, because trim() keeps U+FEFF. In both cases git status, allowed by the other file, goes from exit 0 on develop to exit 3. That is the safe direction, which is why it does not hold this PR.
  • #4652 The Cursor, Gemini and Droid loaders (read_json) still drop an unreadable or unparseable file's rules. Outside this PR's hunks, and the PR body already scopes them out. For Droid, a .factory/settings.json with commandDenylist: ["git push"] plus a trailing comma, or at chmod 000, lets git push origin main be rewritten to rtk git push origin main, where it should be left for Droid's own denylist. Cursor's Shell(git push) deny behaves the same way.
  • #4282 (comment added): a file that parses but has the wrong shape (null, or "deny": "Bash(git push *)" as a string) still drops its rules silently. That code is outside this PR's hunks.

Checked and correct — no need to re-verify

  • A deny rule from another settings file still wins over the fail-closed Ask: rm -rf build exits 2 in every broken state.
  • Valid, missing, empty, whitespace-only, BOM + valid JSON and dangling-symlink files give the same verdicts as develop.
  • rtk hook claude produces the same rewrite as before. The only change is that while a file is broken, an allow rule from another file no longer produces permissionDecision: allow.
  • Claude Code 2.1.303, run alone against a local API stand-in with a Bash(canary *) deny in settings.json, does not apply that deny when the file has a trailing comma (in the object or in the array), a // comment or a /* */ comment: the command runs. It does apply it with a leading BOM, and rtk reads that file too. So no tested file format is one Claude Code applies and rtk cannot read, and with this PR rtk asks in the cases where Claude Code itself drops the file.
  • rtk discover coverage is unchanged, because it treats Ask like Default.
  • Every read failure names the file and the OS error (EACCES, EISDIR, non-UTF-8, ENOTDIR).
  • The PR merges cleanly onto develop e542dca0b. fmt and clippy are clean and the full suite passes on the merge.

Your questions

None asked.

Next

Fixup cd9dff3 pushed and code-reviewed. It adds openclaw_asks_when_a_settings_file_cannot_be_read_or_parsed, which checks through rtk rewrite with RTK_REWRITE_HOST=openclaw that an unreadable file and an unparseable file each give exit 3 and a warning, and test_unparseable_settings_file_keeps_other_files_deny_rules, which checks that an unparseable file does not mask deny rules from the other files. The new test helpers now use expect instead of unwrap. Approving.
Rounds: 1. Threads: 0 open, 0 resolved this round.

@KuSh
KuSh merged commit 83fea35 into rtk-ai:develop Oct 18, 2026
14 checks passed
@rtk-release-bot rtk-release-bot Bot mentioned this pull request Oct 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working permissions security

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants