Skip to content

fix(openclaw): handle exit code 3 from rtk rewrite#2202

Merged
KuSh merged 1 commit into
rtk-ai:developfrom
kzzalews:fix/openclaw-exit-code-3-clean
Jun 30, 2026
Merged

fix(openclaw): handle exit code 3 from rtk rewrite#2202
KuSh merged 1 commit into
rtk-ai:developfrom
kzzalews:fix/openclaw-exit-code-3-clean

Conversation

@kzzalews

@kzzalews kzzalews commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

Problem

The rtk-rewrite OpenClaw plugin silently drops all command rewrites. Every rtk rewrite call that produces a suggestion returns exit code 3, which execSync treats as an exception. The catch block returns null, so no command is ever rewritten.

See #2200 for full analysis.

Fix

Check e.stdout in the catch handler — exit code 3 with stdout output is a valid rewrite suggestion, not an error:

} catch (e: any) {
    if (e?.stdout) {
      const result = String(e.stdout).trim();
      if (result && result !== command) return result;
    }
    return null;
  }

Verified

All common shell commands now correctly rewritten:

git log --oneline -5     → rtk git log --oneline -5     ✅
kubectl get pods -A      → rtk kubectl get pods -A      ✅
cat /tmp/test            → rtk read /tmp/test           ✅
ls -la /tmp              → rtk ls -la /tmp              ✅
find /tmp -name "*.log"  → rtk find /tmp -name "*.log"  ✅
grep -r test /tmp        → rtk grep -r test /tmp        ✅

All previously returned null (no rewrite) with the upstream code.


Aiko — AI assistant in OpenClaw
Running RTK on k3s with Kilo Gateway

Config: RTK 0.42.0 | OpenClaw 2026.5.28 | Node.js v24.14.0 | Model: MiMo-V2.5-Pro

Reviewed and verified by Zal (@kzzalews)

@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.

Thanks for your PR! While your current solution works as a temporary workaround, it’s not yet production-ready. I’ve left some suggestions to help guide you toward a more robust, final implementation.

Let me know if you’d like further clarification or assistance!

Comment thread openclaw/index.ts
@KuSh KuSh self-assigned this Jun 28, 2026
kzzalews added a commit to kzzalews/rtk that referenced this pull request Jun 29, 2026
Address KuSh review feedback on rtk-ai#2202:
- tryRewrite now returns [string | null, "ask"?] tuple
- Exit code 3 specifically checked (e.status === 3)
- before_tool_call returns requireApproval when rewrite
  is a suggestion (exit 3), per OpenClaw hook docs
- 60s approval timeout with deny on timeout

See: rtk-ai#2202 (review)

Signed-off-by: Karol Zalewski <karol.zalewski@4zal.net>
kzzalews added a commit to kzzalews/rtk that referenced this pull request Jun 29, 2026
Address KuSh review feedback on rtk-ai#2202:
- tryRewrite now returns [string | null, "ask"?] tuple
- Exit code 3 specifically checked (e.status === 3)
- before_tool_call returns requireApproval when rewrite
  is a suggestion (exit 3), per OpenClaw hook docs
- 60s approval timeout with deny on timeout

See: rtk-ai#2202 (review)

Signed-off-by: Karol Zalewski <karol.zalewski@4zal.net>
@kzzalews kzzalews force-pushed the fix/openclaw-exit-code-3-clean branch from 1162f2d to 2256da9 Compare June 29, 2026 05:35
kzzalews added a commit to kzzalews/rtk that referenced this pull request Jun 29, 2026
Rework the plugin to respect RTK's full exit code protocol
(src/hooks/rewrite_cmd.rs):

Exit codes:
  0 + stdout  Allow — auto-apply rewrite (no approval)
  1           No match — pass through unchanged
  2           Deny — block the call (new)
  3 + stdout  Ask — rewrite with requireApproval (KuSh review)

Changes:
- tryRewrite returns [string | null, "ask" | "deny" | undefined]
- Exit 3: requireApproval with configurable timeout (default 120s)
- Exit 2: block:true (was silently passed through — security gap)
- allowedDecisions: ["allow-once", "deny"] — no allow-always
  (plugin does not persist trust)
- onResolution callback for verbose logging
- approvalTimeoutMs config option in openclaw.plugin.json
- Documented exit code protocol in file header

Address KuSh review: rtk-ai#2202 (review)

Signed-off-by: Karol Zalewski <karol.zalewski@4zal.net>

@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.

Just a few remaining nitpicks and questions

Comment thread openclaw/index.ts Outdated
Comment thread openclaw/index.ts Outdated
Comment thread openclaw/index.ts
kzzalews added a commit to kzzalews/rtk that referenced this pull request Jun 29, 2026
…t timeoutMs

- Extract RewriteVerdict type per KuSh suggestion
- Remove timeoutMs from requireApproval (omit to use Gateway default)
- Remove approvalTimeoutMs config option from plugin.json
- allowedDecisions still excludes allow-always: OpenClaw docs confirm
  plugin allow-always does not auto-persist; offering it without our own
  persistence would mislead users

Address KuSh review: rtk-ai#2202 (review-4588193872)

Signed-off-by: Karol Zalewski <karol.zalewski@4zal.net>
@kzzalews

Copy link
Copy Markdown
Contributor Author

Thanks for the thorough review, KuSh — really appreciate the guidance.

1. RewriteVerdict type — done in 9827755. Extracted type RewriteVerdict = "ask" | "deny" and updated the tryRewrite signature accordingly.

2. timeoutMs omission — done in the same commit. Removed timeoutMs from requireApproval and the corresponding approvalTimeoutMs config option from openclaw.plugin.json. The approval prompt now falls back to the Gateway's default timeout.

3. allow-always omission — intentional, but I should have documented the reasoning. The OpenClaw docs are explicit that allow-always does not auto-persist for plugin hooks:

"allow-always is only durable when the requesting plugin or runtime implements that persistence. For ordinary before_tool_call.requireApproval hooks, OpenClaw treats allow-once and allow-always as approval decisions for the current call and passes the resolved value to onResolution."
Plugin permission requests — Decision behavior

The troubleshooting section reinforces this:

"allow-always appears but the next call prompts again. The generic plugin approval flow does not automatically persist trust for arbitrary hooks."
Plugin permission requests — Troubleshooting

Since this plugin doesn't implement its own trust persistence, offering allow-always would mislead users — the button would look like it remembers the decision, but the next call would prompt again. I'd rather be honest and offer only allow-once + deny. If persistent trust is desired, I can add a config-based allowlist of trusted command patterns in a follow-up.

@kzzalews kzzalews force-pushed the fix/openclaw-exit-code-3-clean branch from 9827755 to 6b2851c Compare June 29, 2026 09:18
kzzalews added a commit to kzzalews/rtk that referenced this pull request Jun 29, 2026
Address KuSh review feedback on rtk-ai#2202:
- tryRewrite now returns [string | null, "ask"?] tuple
- Exit code 3 specifically checked (e.status === 3)
- before_tool_call returns requireApproval when rewrite
  is a suggestion (exit 3), per OpenClaw hook docs
- 60s approval timeout with deny on timeout

See: rtk-ai#2202 (review)

Signed-off-by: Karol Zalewski <karol.zalewski@4zal.net>
kzzalews added a commit to kzzalews/rtk that referenced this pull request Jun 29, 2026
Rework the plugin to respect RTK's full exit code protocol
(src/hooks/rewrite_cmd.rs):

Exit codes:
  0 + stdout  Allow — auto-apply rewrite (no approval)
  1           No match — pass through unchanged
  2           Deny — block the call (new)
  3 + stdout  Ask — rewrite with requireApproval (KuSh review)

Changes:
- tryRewrite returns [string | null, "ask" | "deny" | undefined]
- Exit 3: requireApproval with configurable timeout (default 120s)
- Exit 2: block:true (was silently passed through — security gap)
- allowedDecisions: ["allow-once", "deny"] — no allow-always
  (plugin does not persist trust)
- onResolution callback for verbose logging
- approvalTimeoutMs config option in openclaw.plugin.json
- Documented exit code protocol in file header

Address KuSh review: rtk-ai#2202 (review)

Signed-off-by: Karol Zalewski <karol.zalewski@4zal.net>
kzzalews added a commit to kzzalews/rtk that referenced this pull request Jun 29, 2026
…t timeoutMs

- Extract RewriteVerdict type per KuSh suggestion
- Remove timeoutMs from requireApproval (omit to use Gateway default)
- Remove approvalTimeoutMs config option from plugin.json
- allowedDecisions still excludes allow-always: OpenClaw docs confirm
  plugin allow-always does not auto-persist; offering it without our own
  persistence would mislead users

Address KuSh review: rtk-ai#2202 (review-4588193872)

Signed-off-by: Karol Zalewski <karol.zalewski@4zal.net>
@kzzalews kzzalews requested a review from KuSh June 29, 2026 09:44
@kzzalews kzzalews force-pushed the fix/openclaw-exit-code-3-clean branch from 6b2851c to c059525 Compare June 29, 2026 10:45
@CLAassistant

CLAassistant commented Jun 29, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@kzzalews kzzalews force-pushed the fix/openclaw-exit-code-3-clean branch 3 times, most recently from f5c6eab to 97aaac8 Compare June 29, 2026 11:38
Comment thread openclaw/index.ts Outdated
Comment thread openclaw/index.ts
@kzzalews kzzalews force-pushed the fix/openclaw-exit-code-3-clean branch from 0d50a72 to 8b632e9 Compare June 30, 2026 05:31
Interpret all RTK rewrite exit codes and respond appropriately:
- Exit 0 (Allow): auto-apply rewrite
- Exit 1 (Passthrough): no RTK equivalent, pass through unchanged
- Exit 2 (Deny): block the call entirely
- Exit 3 (Ask): rewrite available, require user approval via
  requireApproval with allow-once/deny decisions

Uses execFileSync (not execSync) to avoid shell injection. Returns
a [string | null, RewriteVerdict?] tuple from tryRewrite so the
before_tool_call hook can react to each verdict type.

"allow-always" omitted from allowedDecisions because OpenClaw does
not auto-persist approval for plugin hooks — see:
https://docs.openclaw.ai/plugins/plugin-permission-requests#troubleshooting

Closes rtk-ai#2202
@kzzalews kzzalews force-pushed the fix/openclaw-exit-code-3-clean branch from 8b632e9 to 776a4b1 Compare June 30, 2026 05:47
@kzzalews

Copy link
Copy Markdown
Contributor Author

Thanks KuSh! All three rounds of feedback are now addressed in a single squashed commit (776a4b1):

  1. execFileSync restored — shell interpolation removed, .toString() added for Buffer safety
  2. timeoutMs removed — falling back to Gateway's default decision timeout
  3. allow-always omitted — inline comment added explaining OpenClaw doesn't auto-persist trust for plugin hooks
  4. Type safety cleanupse.stdout.toString() for consistency, redundant optional chaining removed, explicit undefined trimmed from tuple returns

@kzzalews kzzalews requested a review from KuSh June 30, 2026 10:15

@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.

LGTM, thanks for keeping up!

@KuSh KuSh merged commit 487a2e7 into rtk-ai:develop Jun 30, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants