Skip to content

fix(doctor): case-insensitive safe-bin trusted dir matching on macOS/Windows#42131

Open
hkochar wants to merge 1 commit intoopenclaw:mainfrom
hkochar:fix/safe-bin-trusted-dirs-case-insensitive
Open

fix(doctor): case-insensitive safe-bin trusted dir matching on macOS/Windows#42131
hkochar wants to merge 1 commit intoopenclaw:mainfrom
hkochar:fix/safe-bin-trusted-dirs-case-insensitive

Conversation

@hkochar
Copy link
Copy Markdown

@hkochar hkochar commented Mar 10, 2026

Summary

normalizeConfiguredSafeBins() can lowercase configured safe-bin paths, while trusted safe-bin directory comparisons were exact-case. On case-insensitive filesystems such as default macOS APFS and typical Windows NTFS, that can produce false "outside trusted safe-bin dirs" doctor/runtime warnings even when safeBinTrustedDirs is configured correctly.

The maintainer update keeps the fix path-local: trusted-dir comparison keys are case-folded only when probing the actual compared path shows the filesystem treats that path case-insensitively. Case-sensitive paths keep exact matching, so case-distinct sibling directories do not inherit trust.

Changes

  • src/infra/exec-safe-bin-trust.ts: normalize trusted-dir comparison keys with path-local case-sensitivity probing instead of a global temp-dir/platform probe.
  • src/infra/exec-safe-bin-trust.ts: keep writable trusted-dir diagnostics reporting the configured resolved path rather than the folded comparison key.
  • src/infra/exec-safe-bin-trust.test.ts: add regression coverage for case-insensitive matching and case-sensitive sibling separation.
  • CHANGELOG.md: add the required fix entry.

Reproduction

  1. On a case-insensitive macOS or Windows filesystem, configure a safe-bin trusted dir with different casing than the resolved executable dirname.
  2. Run the safe-bin trust check through openclaw doctor or the runtime policy path.
  3. Before the fix, the Set.has() comparison can miss because one side is folded and the other preserves case.
  4. After the fix, the compared path is folded only when the actual filesystem path resolves case-insensitively.

Real behavior proof

  • Behavior or issue addressed: Safe-bin trusted-dir comparison accepts different path casing only when the actual filesystem path resolves case-insensitively.
  • Real environment tested: macOS Darwin local maintainer setup, Node v26.0.0, prepared head 290418af5a6516477df180c0526993fb412e87b5.
  • Exact steps or command run after this patch: Created a real temporary directory, built a swapped-case spelling of that directory path, verified the original and swapped paths stat to the same filesystem object, built trusted safe-bin dirs from the swapped-case path, and checked an executable path under the original-case directory with isTrustedSafeBinPath.
  • Evidence after fix: Copied live terminal output from the maintainer machine:
$ pnpm exec tsx -e '...exercise buildTrustedSafeBinDirs/isTrustedSafeBinPath against a real temp dir with swapped path casing...'
{
  "platform": "darwin",
  "dir": "/var/folders/7b/8hcnb089297c2t35zvm__3y00000gn/T/OpenClaw-Safe-Bin-Proof-LFRwff",
  "swapped": "/VAR/FOLDERS/7B/8HCNB089297C2T35ZVM__3Y00000GN/t/oPENcLAW-sAFE-bIN-pROOF-lfrWFF",
  "sameObject": true,
  "trusted": true
}
  • Observed result after fix: The swapped-case trusted dir and original-case resolved executable dirname were treated as the same trusted directory only after the filesystem proved both path spellings point to the same object.
  • What was not tested: Live Windows NTFS/PowerShell proof was not run in this maintainer pass; CI and the recurring tracker automation should keep the PR unmerged until required checks and review gates are green.

Test plan

  • pnpm test src/infra/exec-safe-bin-trust.test.ts src/infra/exec-safe-bin-runtime-policy.test.ts src/commands/doctor-config-flow.safe-bins.test.ts passed before the final base refresh: 33 tests.
  • pnpm test:changed passed before the final base refresh: 346 files / 3454 tests.
  • pnpm exec oxfmt --check CHANGELOG.md src/infra/exec-safe-bin-trust.ts src/infra/exec-safe-bin-trust.test.ts passed after the final rebase.
  • Fresh CI passes on prepared head 290418af5a6516477df180c0526993fb412e87b5.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7d0288852c

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/infra/exec-safe-bin-trust.test.ts Outdated
extraDirs: ["/Users/Dev/Custom/bin"],
});
// Trusted dir should be stored lowercased on case-insensitive FS
expect(dirs.has("/users/dev/custom/bin")).toBe(true);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Normalize Windows test expectations with path.resolve

This assertion hardcodes a POSIX-style path, but on win32 the trusted-dir set stores path.resolve(...).toLowerCase() values with Windows separators (for example \\users\\dev\\custom\\bin), so dirs.has("/users/dev/custom/bin") is always false and the test fails on Windows. The checks-windows matrix in .github/workflows/ci.yml runs pnpm test (lines 384-416), so this introduces a deterministic CI failure there.

Useful? React with 👍 / 👎.

Comment thread src/infra/exec-safe-bin-trust.ts Outdated
// macOS (APFS) and Windows (NTFS) use case-insensitive filesystems.
// normalizeConfiguredSafeBins() lowercases bin paths, so trusted dirs
// must also be lowercased for the Set membership check to match.
const CASE_INSENSITIVE_FS = process.platform === "darwin" || process.platform === "win32";
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Detect case sensitivity before lowercasing trust paths

Using process.platform to force case-insensitive matching on all macOS paths is unsafe because APFS can be configured as case-sensitive; on those systems, lowercasing collapses distinct directories into the same key, so binaries outside the explicitly trusted path can be incorrectly treated as trusted (and writable-dir checks can miss real directories). This should be gated by actual filesystem behavior rather than platform alone.

Useful? React with 👍 / 👎.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Mar 10, 2026

Greptile Summary

This PR fixes a real bug on macOS (APFS) and Windows (NTFS) where case-insensitive filesystem handling was broken. The issue: normalizeConfiguredSafeBins() lowercased safe-bin paths, but normalizeTrustedDir() preserved the original case, causing Set.has() lookups to fail and produce spurious "outside trusted safe-bin dirs" doctor warnings.

The fix is minimal and correct:

  • Adds a CASE_INSENSITIVE_FS constant to identify darwin/win32 platforms
  • Applies lowercasing consistently in both normalizeTrustedDir() (write-time) and isTrustedSafeBinPath() (read-time)
  • Matches the established pattern already used in exec-command-resolution.ts
  • Includes two new it.runIf tests to verify case-insensitive matching on darwin/win32
  • All existing tests updated correctly for the lowercased Set entries

The fix is logically sound and well-tested. No logic bugs or functional issues identified.

Confidence Score: 5/5

  • Safe to merge. The fix is logically correct, well-tested, and follows established patterns in the codebase.
  • The root cause of the case-insensitive filesystem bug is clearly identified and correctly fixed. Both sides of the Set comparison are now consistently lowercased on darwin/win32. All test cases pass and are properly updated. The implementation matches the existing pattern in exec-command-resolution.ts. No functional issues, logic errors, or regressions identified.
  • No files require special attention.

Last reviewed commit: 7d02888

@hkochar
Copy link
Copy Markdown
Author

hkochar commented Mar 10, 2026

Addressed both review comments:

P1 (Windows paths): Fixed in e8b0655 — tests now use path.join(path.sep, ...) and path.resolve(...).toLowerCase() instead of hardcoded POSIX paths, so they work with Windows backslash separators.

P2 (Case-sensitive APFS): Valid point in theory — APFS can be configured as case-sensitive. In practice this is extremely rare (macOS default is case-insensitive, and Disk Utility warns against it for compatibility). The existing normalizeMatchTarget() in exec-command-resolution.ts already lowercases unconditionally on all non-win32 platforms, so this PR is consistent with the codebase pattern. A runtime fs.realpathSync probe would be more correct but adds I/O on every trust check — happy to add it if maintainers prefer that approach.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e8b0655341

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/infra/exec-safe-bin-trust.ts Outdated
// macOS (APFS) and Windows (NTFS) use case-insensitive filesystems.
// normalizeConfiguredSafeBins() lowercases bin paths, so trusted dirs
// must also be lowercased for the Set membership check to match.
const CASE_INSENSITIVE_FS = process.platform === "darwin" || process.platform === "win32";
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Detect filesystem case sensitivity before lowercasing trust paths

Lowercasing trusted directories based only on process.platform breaks trust boundaries on macOS volumes formatted as case-sensitive APFS: distinct paths like /opt/Tools/bin and /opt/tools/bin collapse to the same key, so isTrustedSafeBinPath can treat binaries from an untrusted sibling directory as trusted (and listWritableExplicitTrustedSafeBinDirs can miss real directories when fs.statSync is run on the lowercased, non-existent path). This is introduced by forcing case-insensitive normalization for all darwin hosts instead of detecting actual filesystem behavior.

Useful? React with 👍 / 👎.

@hkochar
Copy link
Copy Markdown
Author

hkochar commented Mar 10, 2026

P2→P1 (Case-sensitive APFS): Addressed in 8ea4f62. Replaced the static process.platform heuristic with a runtime inode-based filesystem probe:

function detectCaseInsensitiveFs(): boolean {
  const tmpDir = os.tmpdir();
  const original = fs.statSync(tmpDir);
  const flipped = fs.statSync(tmpDir.toUpperCase());
  return original.ino === flipped.ino && original.dev === flipped.dev;
}

This correctly handles case-sensitive APFS volumes on macOS — if the FS is case-sensitive, no lowercasing occurs. Falls back to the platform heuristic if the probe fails. Tests updated to use the same probe for expectations.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8ea4f628be

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/infra/exec-safe-bin-trust.ts Outdated
Comment on lines +67 to +68
const resolved = path.resolve(trimmed);
return CASE_INSENSITIVE_FS ? resolved.toLowerCase() : resolved;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Determine case sensitivity per trusted path, not tmpdir

normalizeTrustedDir applies lowercasing based on the single CASE_INSENSITIVE_FS probe from os.tmpdir(), so when the temp volume is case-insensitive but a configured safeBinTrustedDirs entry lives on a case-sensitive mount (for example a case-sensitive APFS volume under /Volumes), distinct directories that differ only by case collapse to the same trust key. In that setup, isTrustedSafeBinPath can incorrectly accept binaries from an untrusted sibling path (and writable-dir checks can miss the intended directory), which weakens the trust boundary this check is meant to enforce.

Useful? React with 👍 / 👎.

@hkochar
Copy link
Copy Markdown
Author

hkochar commented Mar 10, 2026

Re: per-path case sensitivity detection — acknowledged. A per-path probe (stat the directory itself with flipped case) would handle cross-volume mounts with mixed case sensitivity. However this adds a statSync pair per trusted dir entry on every build, and the scenario (tmpdir on case-insensitive APFS, safeBins on a case-sensitive secondary mount) is extremely niche.

Happy to add per-path probing if maintainers want it, but I'd suggest landing the tmpdir probe first as a significant improvement over the status quo (no case handling at all) and iterating if the edge case comes up in practice.

@openclaw-barnacle
Copy link
Copy Markdown

This pull request has been automatically marked as stale due to inactivity.
Please add updates or it will be closed.

@openclaw-barnacle openclaw-barnacle Bot added the stale Marked as stale due to inactivity label Apr 27, 2026
@clawsweeper
Copy link
Copy Markdown
Contributor

clawsweeper Bot commented Apr 27, 2026

Codex review: needs maintainer review before merge.

Summary
The PR adds path-local case-sensitivity probing for safe-bin trusted-dir comparisons, preserves original paths for writable-dir diagnostics, adds regression tests, and updates the changelog.

Reproducibility: yes. by source inspection: current main lowercases configured safeBins, preserves trusted-dir case, and then exact-matches the resolved executable directory. I did not run a live macOS or Windows doctor reproduction in this read-only review.

Real behavior proof
Sufficient (terminal): Sufficient: the PR body includes copied live macOS terminal output showing a real swapped-case temp dir statting to the same object and trusted: true after the patch.

Next step before merge
Manual CI handling remains because the exact PR head has a failing core check with no actionable annotation; this review found no narrow repairable code defect.

Security
Cleared: Cleared: the diff changes local path normalization, tests, and changelog only; it adds no dependencies, workflows, secrets handling, or new external code execution sources.

Review details

Best possible solution:

Land this path-local safe-bin trust fix after exact-head CI is clean, keeping the comparison logic in src/infra/exec-safe-bin-trust.ts and preserving case-sensitive directory separation.

Do we have a high-confidence way to reproduce the issue?

Yes, by source inspection: current main lowercases configured safeBins, preserves trusted-dir case, and then exact-matches the resolved executable directory. I did not run a live macOS or Windows doctor reproduction in this read-only review.

Is this the best way to solve the issue?

Yes: the PR fixes the comparison at the trust helper with path-local filesystem probing, avoids a broad platform/tmpdir heuristic, and keeps writable-dir diagnostics on original paths. The remaining blocker is CI/branch validation, not a better code direction.

What I checked:

  • current-main-trusted-dirs-exact-match: Current main resolves trusted safe-bin dirs with path.resolve() and checks exact Set membership against the resolved executable directory, which leaves mixed-case trusted dirs unmatched on case-insensitive filesystems. (src/infra/exec-safe-bin-trust.ts:31, f1ced1961a2f)
  • current-main-safe-bins-lowercase: The doctor safe-bin scan lowercases configured safeBins before checking resolved executables, so the reported mismatch is source-reproducible when trusted-dir casing differs from the resolved executable dirname. (src/commands/doctor/shared/exec-safe-bins.ts:46, f1ced1961a2f)
  • documented-trust-boundary: The public docs define safe bins as resolving only from system defaults plus optional tools.exec.safeBinTrustedDirs, with PATH entries never auto-trusted, so this comparison is part of the exec trust boundary. Public docs: docs/tools/exec-approvals-advanced.md. (docs/tools/exec-approvals-advanced.md:68, f1ced1961a2f)
  • pr-head-path-local-case-folding: The PR head adds pathCaseInsensitive() and normalizeTrustComparisonPath(), folding comparison keys only after statting the compared path or nearest existing parent as the same filesystem object. (src/infra/exec-safe-bin-trust.ts:42, 290418af5a6f)
  • pr-head-regression-tests: The PR adds one test that accepts swapped-case trusted dirs only when both spellings stat to the same object, and another that keeps case-distinct sibling dirs separate when the filesystem permits both names. (src/infra/exec-safe-bin-trust.test.ts:74, 290418af5a6f)
  • real-behavior-proof-supplied: The PR body now includes copied live macOS terminal output showing a swapped-case temp dir statting as the same object and isTrustedSafeBinPath returning trusted: true after the patch. (290418af5a6f)

Likely related people:

  • steipete: Authored the safeBins trust resolver extraction, explicit safeBinTrustedDirs hardening, trusted-dir normalization refactor, and the current blamed safe-bin trust implementation in this checkout. (role: introduced behavior and recent area contributor; confidence: high; commits: ac0db68235a0, 64b273a71cf0, 862975507abe; files: src/infra/exec-safe-bin-trust.ts, src/infra/exec-safe-bin-runtime-policy.ts, src/infra/exec-command-resolution.ts)
  • BradGroux: Authored the latest PR head commit and supplied the maintainer update/proof on the active branch, so he is the clearest route for branch mechanics and CI follow-up. (role: prepared branch follow-up owner; confidence: medium; commits: 290418af5a6f; files: src/infra/exec-safe-bin-trust.ts, src/infra/exec-safe-bin-trust.test.ts, CHANGELOG.md)

Remaining risk / open question:

  • Fresh exact-head CI is not green: checks-node-core failed with only a generic annotation in the available check-run data.
  • The PR body's proof text appears to contain a mistyped prepared-head SHA even though the live PR head is 290418a; correcting that would improve audit clarity.

Codex review notes: model gpt-5.5, reasoning high; reviewed against f1ced1961a2f.

Re-review progress:

@openclaw-barnacle openclaw-barnacle Bot removed the stale Marked as stale due to inactivity label Apr 28, 2026
@BradGroux BradGroux force-pushed the fix/safe-bin-trusted-dirs-case-insensitive branch from 8ea4f62 to af4a016 Compare May 10, 2026 05:02
@openclaw-barnacle openclaw-barnacle Bot added the triage: needs-real-behavior-proof Candidate: external PR needs after-fix proof from a real setup. label May 10, 2026
@BradGroux BradGroux force-pushed the fix/safe-bin-trusted-dirs-case-insensitive branch from af4a016 to 61ce081 Compare May 10, 2026 05:11
@BradGroux
Copy link
Copy Markdown
Member

Prepared maintainer update at 61ce0816950ddfac52b384c4484938d2d15ab4d7 after rebasing onto current main.

Changes made:

  • Replaced the global temp-dir case probe with path-local case folding for the trusted-dir comparison key.
  • Kept writable trusted-dir diagnostics reporting the configured resolved path instead of the folded comparison key.
  • Added regression coverage for case-insensitive matching and case-sensitive sibling separation.
  • Added the required changelog entry.

Verification:

  • pnpm test src/infra/exec-safe-bin-trust.test.ts src/infra/exec-safe-bin-runtime-policy.test.ts src/commands/doctor-config-flow.safe-bins.test.ts passed before the final base refresh: 33 tests.
  • pnpm test:changed passed before the final base refresh: 346 files / 3454 tests.
  • pnpm exec oxfmt --check CHANGELOG.md src/infra/exec-safe-bin-trust.ts src/infra/exec-safe-bin-trust.test.ts passed after the final rebase.

Fresh GitHub checks are queued on the prepared head; I did not merge while those are pending.

@clawsweeper re-review

@clawsweeper
Copy link
Copy Markdown
Contributor

clawsweeper Bot commented May 10, 2026

🦞🧹
ClawSweeper re-review requested.

I asked ClawSweeper to review this item again.
Action: item re-review queued (workflow sweep.yml, event repository_dispatch).
Result: the existing ClawSweeper review comment will be edited in place when the review finishes.

@openclaw-barnacle openclaw-barnacle Bot added proof: supplied External PR includes structured after-fix real behavior proof. and removed triage: needs-real-behavior-proof Candidate: external PR needs after-fix proof from a real setup. labels May 10, 2026
@BradGroux BradGroux force-pushed the fix/safe-bin-trusted-dirs-case-insensitive branch from 61ce081 to 290418a Compare May 10, 2026 05:18
@BradGroux
Copy link
Copy Markdown
Member

Updated prepared head to 290418af5a6516477df180c0526993fb412e87b5.

Follow-up from CI: after the final base rebase, exec-safe-bin-trust.test.ts had two stale os.tmpdir() references because current main now uses the shared withTempDir helper. I changed the new tests to use withTempDir and force-pushed the prepared branch.

Real behavior proof now passes on the PR body. Fresh CI is running; I am not merging while checks are pending/failing.

@clawsweeper clawsweeper Bot added the proof: sufficient ClawSweeper judged the real behavior proof convincing. label May 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

proof: sufficient ClawSweeper judged the real behavior proof convincing. proof: supplied External PR includes structured after-fix real behavior proof. size: S

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants