rtk-ai / rtk-ai/rtk

rewrite: host-scoped permission authority so plugin hosts stop adding a second approval gate

Open
#3,908 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Rust
Stars
81.1k
Forks
5.1k
Avg merge
4d 21h
Merged PRs (30d)
35

Description

Summary

rtk rewrite evaluates every command against Claude Code's permission files regardless of which agent called it. For OpenClaw this turns routine rewrites into blocking approval prompts on a host whose own exec policy already allows the command. Add a host parameter to rtk rewrite, register OpenClaw as a host whose permission authority is the host itself, and drop requireApproval from the OpenClaw plugin. Behavior for every existing caller is unchanged.

Problem statement

rewrite_cmd::run calls check_command(cmd), which src/hooks/permissions.rs defines as check_command_for(cmd, Host::Claude). The Host enum already carries Claude, Cursor, Gemini, Droid, and Vibe, and rtk rewrite can select none of them: Commands::Rewrite declares args and nothing else.

When the OpenClaw plugin calls rtk rewrite <command>, RTK reads the four Claude Code settings files that load_permission_rules builds and applies Claude Code's least-privilege default to a runtime that is not Claude Code. With no matching allow rule the verdict is Default, Default exits 3, and openclaw/index.ts:132 converts exit 3 into a blocking plugin approval with timeoutBehavior: "deny" and allowedDecisions: ["allow-once", "deny"].

Reproduced on OpenClaw 2026.7.1-2 with tools.exec.mode=full, effective security=full, and ask=off. Every rewritable command stops for a 120-second approval derived from a different agent's config file, which blocks unattended work that OpenClaw's own exec policy permits.

OpenClaw is the only adapter that gates

Host Adapter Exit 3 Exit 2
Pi hooks/pi/rtk.ts rewrite, no gate passthrough
Hermes hooks/hermes/rtk-rewrite/__init__.py rewrite, no gate passthrough
OpenCode hooks/opencode/rtk.ts rewrite, no gate passthrough
Mistral Vibe src/hooks/hook_cmd.rs, run_vibe_inner rewrite, no gate deny
OpenClaw openclaw/index.ts:132 blocking approval block

hooks/pi/README.md states the convention: "Exit codes 0 and 3 both mean 'rewrite and allow'; they are handled identically."

vscode_response_from_decision in src/hooks/hook_cmd.rs records the same failure in another host. In #3037, asserting "ask" made Copilot CLI 1.0.66+ force a blocking dialog with no remember option on every rewritten command, and the fix was to stop asserting and defer to the host's native flow.

Commit 487a2e7, fix(openclaw): handle exit code 3 from rtk rewrite from PR #2202, added the OpenClaw gate on purpose, so this proposal revises a decision that was made deliberately. The argument for revising it is that exit 3 was read as a permission decision when OpenClaw consumes it as a rewrite decision and then applies its own exec policy.

Strategy

Add a host parameter to rtk rewrite and a host whose permission authority is the host itself, mirroring the Host::Vibe case in load_rules_for.

src/hooks/permissions.rs gains Host::OpenClaw, resolving to an empty rule triple. RTK owns no OpenClaw rule source, so it asserts no verdict of its own.

src/hooks/rewrite_cmd.rs gains evaluate_for(cmd, host, excluded, prefixes), and evaluate becomes the Host::Claude case of it. For a host-authoritative host, Ask and Default both resolve to Allow, so a rewritable command exits 0. Deny still exits 2. contains_unattestable_construct still short-circuits to Passthrough before any of this, so the substitution and redirect surface stays closed for every host.

openclaw/index.ts passes the host, removes the requireApproval block and the "ask" verdict arm, and keeps the exit-2 deny branch.

Docs in openclaw/README.md and docs/guide/getting-started/supported-agents.md state that OpenClaw's exec policy is the sole authority over whether a rewritten command runs.

Interface

Preferred form is rtk rewrite --host openclaw <command>. The positional is declared trailing_var_arg = true, allow_hyphen_values = true so that rtk rewrite ls -al parses, and adding an option to that command needs a parse test of its own. If the option cannot be added without breaking that case, use RTK_REWRITE_HOST=openclaw, which carries no clap interaction.

Security invariant

src/hooks/rewrite_cmd.rs carries a test module exit_code_protocol whose comment says PermissionVerdict::Default must map to exit 3 and never to exit 0, citing #1155. This proposal maps Default to exit 0 for one host, so the invariant needs restating as host-scoped rather than global.

Claude Code consumes the exit code as a permission decision, so collapsing Default to allow there would remove the only gate. OpenClaw consumes it as a rewrite decision and then applies tools.exec.mode and security to the resulting command, so the gate survives in the host.

Every existing #1155 test stays verbatim under Host::Claude. The OpenClaw case gets its own named invariant beside them.

Scope

In scope: the host parameter, Host::OpenClaw, the plugin change, and the docs updates.

Out of scope: migrating Pi, Hermes, and OpenCode onto the host parameter. They already behave correctly through adapter code, and moving them is a follow-up that would let those adapters delete their exit-3 special cases.

Out of scope: a permission_authority key in HooksConfig. A global setting would reach the Claude Code hook path and weaken it.

Tests

src/hooks/rewrite_cmd.rs:

  • Host::OpenClaw with verdict Default gives Allow.
  • Host::Claude with verdict Default still gives Ask, guarding #1155.
  • An unattestable construct under Host::OpenClaw gives Passthrough and never Allow.
  • A host with matching deny rules still gives Deny.

src/hooks/permissions.rs:

  • Host::OpenClaw yields an empty rule triple, pinned beside the Host::Vibe case.

src/main.rs:

  • Parse tests for the host argument.
  • Regression cases proving rtk rewrite ls -al and rtk rewrite head -50 file.txt still parse with the option declared.

openclaw/:

  • The directory has no test harness today. Either add a first test for the decision mapping, following hooks/hermes/tests/, or state that the Rust tests are the only coverage.

Backward compatibility

rtk rewrite <cmd> with no host argument stays byte-identical, so no existing adapter needs a change.

Old plugin with new rtk gives today's behavior.

New plugin with old rtk gives exit 1 passthrough. Verified against rtk 0.45.0: rtk rewrite --host openclaw "git status" exits 1 with empty stdout and empty stderr, because the unknown flag is swallowed by the trailing positional. Token savings are lost until rtk is upgraded, and nothing blocks or denies. Adding a version probe next to the existing which rtk check in openclaw/index.ts would turn that silent loss into a warning.

Known limitation

The plugin mutates params.command in before_tool_call, and OpenClaw folds hook adjustments into executeParams before calling tool.execute (observed in the installed OpenClaw 2026.7.1-2 bundle, dist/tool-split-BONVLU1k.js), so the exec tool receives rtk git push rather than git push. Whether OpenClaw's exec policy is evaluated before or after that adjustment is unverified. If it is evaluated after, a user deny rule written against the raw command stops matching.

This applies today on the exit-0 allow path and is not introduced by this change. Docs should tell OpenClaw users to write exec rules against the rtk form.

Assumption open to review

Host::OpenClaw carries no deny rules, matching Host::Vibe. An OpenClaw user today gets .claude/settings.json deny rules enforced against OpenClaw as a side effect of the hardcoded host, and this change removes that. Nothing in the repo documents or claims that behavior.

The alternative keeps RTK reading Claude deny rules for OpenClaw while collapsing only Ask and Default. Choose it if any OpenClaw user is knowingly relying on .claude/settings.json to gate OpenClaw.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with src/hooks/permissions.rs, src/hooks/rewrite_cmd.rs, and src/main.rs, then inspect openclaw/index.ts and the existing exit_code_protocol tests. Run the listed parse and permission tests first, including the rtk rewrite ls -al cases. Done means host-scoped behavior is covered, existing Claude behavior remains unchanged, the OpenClaw plugin no longer adds its approval gate, and the two documentation pages describe the authority model.

Written by the indexing model from the issue text.

Assessment

Tech stack
rust, typescript
Domain
cli, documentation, testing, tooling
Issue type
Feature
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
48/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.