fix(exec): respect OPENCLAW_STATE_DIR for exec approvals#74002
fix(exec): respect OPENCLAW_STATE_DIR for exec approvals#74002openclaw-clownfish[bot] wants to merge 1 commit into
Conversation
🔒 Aisle Security AnalysisWe found 2 potential security issue(s) in this PR:
1. 🟡 Exec approvals policy can be silently bypassed by OPENCLAW_STATE_DIR pointing to a fresh directory
Description
This weakens local exec protections because the newly-created file has no explicit Impact:
Vulnerable flow:
Vulnerable code: const override = env.OPENCLAW_STATE_DIR?.trim();
if (override) {
const resolved = resolveHomeRelativePath(override, { env });
return { path: resolved, displayPath: resolved };
}
...
export function ensureExecApprovals(): ExecApprovalsFile {
const loaded = loadExecApprovals();
...
saveExecApprovals(updated);
return updated;
}RecommendationPrevent accidental policy downgrades when switching state roots. Options (pick one):
Example migration logic: const legacyPath = path.join(expandHomePrefix("~/.openclaw"), "exec-approvals.json");
const newPath = resolveExecApprovalsPath();
if (process.env.OPENCLAW_STATE_DIR && !fs.existsSync(newPath) && fs.existsSync(legacyPath)) {
// copy or merge
fs.mkdirSync(path.dirname(newPath), { recursive: true });
fs.copyFileSync(legacyPath, newPath, fs.constants.COPYFILE_EXCL);
console.warn(`Migrated exec approvals from ${legacyPath} to ${newPath}`);
}Also consider writing explicit defaults into a newly-created approvals file (rather than relying on permissive runtime fallbacks), so the user can see and audit the effective policy. 2. 🟡 Local exec-approvals socket spoofing when OPENCLAW_STATE_DIR points to shared/writable directories
Description
Why this is a security issue:
Vulnerable code (path override + socket usage): const override = env.OPENCLAW_STATE_DIR?.trim();
...
return path.join(resolveExecApprovalsStateDir().path, EXEC_APPROVALS_SOCKET);const payload = JSON.stringify({ type: "request", token, ... });
return await requestJsonlSocket({ socketPath, requestLine: payload, ... });Impact:
RecommendationHarden the socket location and add peer validation:
Example (pre-connect check): import os from "node:os";
function assertSafeSocketPath(socketPath: string) {
const st = fs.lstatSync(socketPath);
if (!st.isSocket()) throw new Error(`Not a socket: ${socketPath}`);
if ((st.mode & 0o022) !== 0) throw new Error(`Socket is writable by group/others: ${socketPath}`);
if (typeof st.uid === "number" && st.uid !== os.userInfo().uid) {
throw new Error(`Socket not owned by current user: ${socketPath}`);
}
}
assertSafeSocketPath(socketPath);And when creating the state directory, set restrictive perms: fs.mkdirSync(stateDir, { recursive: true, mode: 0o700 });Analyzed PR: #74002 at commit Last updated on: 2026-04-29T02:47:12Z |
Greptile SummaryThis PR adds Confidence Score: 4/5Safe to merge; only minor style concerns found, no functional or security regressions. No P0 or P1 issues found. Two P2 style issues: inconsistent path-separator usage in resolveExecApprovalsDisplayPath and a misleading test fixture that creates .clawdbot with no corresponding implementation logic. Core env-resolution, fallback, and symlink-safety logic are correct. src/infra/exec-approvals.ts (display-path separator), src/infra/exec-approvals-store.test.ts (legacy test fixture) Prompt To Fix All With AIThis is a comment left during a code review.
Path: src/infra/exec-approvals.ts
Line: 213-215
Comment:
**Inconsistent path separator in display path**
When the state dir is the default (`~/.openclaw`), the function returns a hardcoded forward-slash string literal, while the non-default branch uses `path.join`. On Windows, the default branch would always emit a forward slash while the `OPENCLAW_STATE_DIR` branch would use backslashes — the two display paths would use different separators for the same purpose. Consider using `path.join` in both branches for consistency, or explicitly document that the default display path is intentionally kept in Unix format to match the docs.
```suggestion
return path.join(stateDir, EXEC_APPROVALS_FILE);
```
How can I resolve this? If you propose a fix, please make it concise.
---
This is a comment left during a code review.
Path: src/infra/exec-approvals-store.test.ts
Line: 120-132
Comment:
**Misleading test fixture — `.clawdbot` directory has no effect**
The test name says "when only legacy state exists" and creates `path.join(dir, ".clawdbot")`, implying there is logic that reacts to a legacy `.clawdbot` directory. However, nothing in the implementation inspects `.clawdbot`; `resolveExecApprovalsPath()` only checks `OPENCLAW_STATE_DIR`. The test passes identically without the `mkdirSync` call and could mislead a future maintainer into searching for legacy-detection code that doesn't exist. Consider renaming the test to describe the actual invariant ("uses the default .openclaw path when OPENCLAW_STATE_DIR is unset") and removing the unused `.clawdbot` setup.
How can I resolve this? If you propose a fix, please make it concise.Reviews (1): Last reviewed commit: "fix(exec): respect OPENCLAW_STATE_DIR fo..." | Re-trigger Greptile |
| return stateDir === DEFAULT_EXEC_APPROVALS_STATE_DIR | ||
| ? `${stateDir}/${EXEC_APPROVALS_FILE}` | ||
| : path.join(stateDir, EXEC_APPROVALS_FILE); |
There was a problem hiding this comment.
Inconsistent path separator in display path
When the state dir is the default (~/.openclaw), the function returns a hardcoded forward-slash string literal, while the non-default branch uses path.join. On Windows, the default branch would always emit a forward slash while the OPENCLAW_STATE_DIR branch would use backslashes — the two display paths would use different separators for the same purpose. Consider using path.join in both branches for consistency, or explicitly document that the default display path is intentionally kept in Unix format to match the docs.
| return stateDir === DEFAULT_EXEC_APPROVALS_STATE_DIR | |
| ? `${stateDir}/${EXEC_APPROVALS_FILE}` | |
| : path.join(stateDir, EXEC_APPROVALS_FILE); | |
| return path.join(stateDir, EXEC_APPROVALS_FILE); |
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/infra/exec-approvals.ts
Line: 213-215
Comment:
**Inconsistent path separator in display path**
When the state dir is the default (`~/.openclaw`), the function returns a hardcoded forward-slash string literal, while the non-default branch uses `path.join`. On Windows, the default branch would always emit a forward slash while the `OPENCLAW_STATE_DIR` branch would use backslashes — the two display paths would use different separators for the same purpose. Consider using `path.join` in both branches for consistency, or explicitly document that the default display path is intentionally kept in Unix format to match the docs.
```suggestion
return path.join(stateDir, EXEC_APPROVALS_FILE);
```
How can I resolve this? If you propose a fix, please make it concise.| it("keeps the default approvals path in .openclaw when only legacy state exists", () => { | ||
| const dir = createHomeDir(); | ||
| fs.mkdirSync(path.join(dir, ".clawdbot"), { recursive: true }); | ||
|
|
||
| expect(path.normalize(resolveExecApprovalsPath())).toBe( | ||
| path.normalize(path.join(dir, ".openclaw", "exec-approvals.json")), | ||
| ); | ||
|
|
||
| ensureExecApprovals(); | ||
|
|
||
| expect(fs.existsSync(approvalsFilePath(dir))).toBe(true); | ||
| expect(fs.existsSync(path.join(dir, ".clawdbot", "exec-approvals.json"))).toBe(false); | ||
| }); |
There was a problem hiding this comment.
Misleading test fixture —
.clawdbot directory has no effect
The test name says "when only legacy state exists" and creates path.join(dir, ".clawdbot"), implying there is logic that reacts to a legacy .clawdbot directory. However, nothing in the implementation inspects .clawdbot; resolveExecApprovalsPath() only checks OPENCLAW_STATE_DIR. The test passes identically without the mkdirSync call and could mislead a future maintainer into searching for legacy-detection code that doesn't exist. Consider renaming the test to describe the actual invariant ("uses the default .openclaw path when OPENCLAW_STATE_DIR is unset") and removing the unused .clawdbot setup.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/infra/exec-approvals-store.test.ts
Line: 120-132
Comment:
**Misleading test fixture — `.clawdbot` directory has no effect**
The test name says "when only legacy state exists" and creates `path.join(dir, ".clawdbot")`, implying there is logic that reacts to a legacy `.clawdbot` directory. However, nothing in the implementation inspects `.clawdbot`; `resolveExecApprovalsPath()` only checks `OPENCLAW_STATE_DIR`. The test passes identically without the `mkdirSync` call and could mislead a future maintainer into searching for legacy-detection code that doesn't exist. Consider renaming the test to describe the actual invariant ("uses the default .openclaw path when OPENCLAW_STATE_DIR is unset") and removing the unused `.clawdbot` setup.
How can I resolve this? If you propose a fix, please make it concise.|
Codex review: found issues before merge. Reviewed May 30, 2026, 1:01 AM ET / 05:01 UTC. Summary PR surface: Source +26, Tests +72, Docs +9. Total +107 across 6 files. Reproducibility: yes. for source proof, not live execution: current main hardcodes exec approvals to Review metrics: 2 noteworthy metrics.
Merge readiness Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch. Rank-up moves:
Risk before merge
Maintainer options:
Next step before merge
Security Review findings
Review detailsBest possible solution: Land a state-dir-aware exec approvals fix only after it preserves or explicitly migrates existing approval policy, removes release-owned changelog churn, and refreshes against current main. Do we have a high-confidence way to reproduce the issue? Yes for source proof, not live execution: current main hardcodes exec approvals to Is this the best way to solve the issue? No. Routing the path through the state directory is the right direction for fresh setups, but this implementation needs an upgrade-safe migration, warning, or fail-closed behavior before it is the best fix. Full review comments:
Overall correctness: patch is incorrect AGENTS.md: found and applied where relevant. Codex review notes: model gpt-5.5, reasoning high; reviewed against b9933b2ec119. Label changesLabel changes:
Label justifications:
Evidence reviewedPR surface: Source +26, Tests +72, Docs +9. Total +107 across 6 files. View PR surface stats
Security concerns:
What I checked:
Likely related people:
What the crustacean ranks mean
Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics. How this review workflow works
|
|
This pull request has been automatically marked as stale due to inactivity. |
Summary
Source PR: #65736
Credit: original fix by @oinoom, with ProjectClownfish repair on the existing branch.
Validation:
ProjectClownfish replacement details:
! [remote rejected] HEAD -> fix/exec-approvals-state-dir (refusing to allow a GitHub App to create or update workflow
.github/workflows/ci.ymlwithoutworkflowspermission)error: failed to push some refs to 'https://github.com/oinoom/openclaw.git'