Skip to content

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

Open
kzzalews wants to merge 1 commit into
rtk-ai:developfrom
kzzalews:fix/openclaw-exit-code-3-clean
Open

fix(openclaw): handle exit code 3 from rtk rewrite#2202
kzzalews wants to merge 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

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
Comment thread openclaw/index.ts Outdated
Comment thread openclaw/index.ts
severity: "info",
timeoutMs: approvalTimeoutMs,
timeoutBehavior: "deny",
allowedDecisions: ["allow-once", "deny"],

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.

Why did you omit "allow-always"?

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.

Still missing a comment explaining the absence of "allow-always"

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
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 2 times, most recently from bbc694c to f5c6eab Compare June 29, 2026 11:33
rtk rewrite returns exit code 3 when it has a rewrite suggestion.
execSync treats non-zero exit codes as errors and throws.
The catch block returned null, silently dropping all rewrites.

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
  3 + stdout  Ask — rewrite with requireApproval

Implementation:
- tryRewrite returns [string | null, RewriteVerdict] tuple
- Exit 3: requireApproval (no allow-always; plugin does not persist trust)
- Exit 2: block:true (was silently passed through — security gap)
- allowedDecisions: ["allow-once", "deny"]
- RewriteVerdict type extracted per KuSh review
- timeoutMs omitted from requireApproval (use Gateway default)

Closes rtk-ai#2200

Signed-off-by: Karol Zalewski <karol.zalewski@4zal.net>
@kzzalews kzzalews force-pushed the fix/openclaw-exit-code-3-clean branch from f5c6eab to 97aaac8 Compare June 29, 2026 11:38
Comment thread openclaw/index.ts
Comment on lines +53 to +69
}).trim();
// Exit 0 — Allow: rewrite and auto-apply
return [result && result !== command ? result : null, undefined];
} catch (e: any) {
// Exit 3 — Ask: rewrite available but user must approve
if (e?.status === 3 && e?.stdout) {
const result = String(e.stdout).trim();
if (result && result !== command) return [result, "ask"];
// Exit 3 but no usable stdout — treat as passthrough
return [null, undefined];
}
// Exit 2 — Deny: command matched a deny rule, block the call
if (e?.status === 2) {
return [null, "deny"];
}
// Exit 1 or unknown — no rewrite, pass through
return [null, undefined];

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.

One regression (execFileSync return value can be a Buffer) and some code cleanups / nitpicks

Suggested change
}).trim();
// Exit 0 — Allow: rewrite and auto-apply
return [result && result !== command ? result : null, undefined];
} catch (e: any) {
// Exit 3 — Ask: rewrite available but user must approve
if (e?.status === 3 && e?.stdout) {
const result = String(e.stdout).trim();
if (result && result !== command) return [result, "ask"];
// Exit 3 but no usable stdout — treat as passthrough
return [null, undefined];
}
// Exit 2 — Deny: command matched a deny rule, block the call
if (e?.status === 2) {
return [null, "deny"];
}
// Exit 1 or unknown — no rewrite, pass through
return [null, undefined];
})
.toString()
.trim();
// Exit 0 — Allow: rewrite and auto-apply
return [result && result !== command ? result : null];
} catch (e: any) {
// Exit 3 — Ask: rewrite available but user must approve
if (e?.status === 3 && e.stdout) {
const result = e.stdout.toString().trim();
if (result && result !== command) return [result, "ask"];
// Exit 3 but no usable stdout — treat as passthrough
return [null];
}
// Exit 2 — Deny: command matched a deny rule, block the call
if (e?.status === 2) {
return [null, "deny"];
}
// Exit 1 or unknown — no rewrite, pass through
return [null];

Comment thread openclaw/index.ts
severity: "info",
timeoutMs: approvalTimeoutMs,
timeoutBehavior: "deny",
allowedDecisions: ["allow-once", "deny"],

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.

Still missing a comment explaining the absence of "allow-always"

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