(resolvedPath: string)
| 99 | * .claude/settings.json) are still blocked even if their parent is in allowOnly. |
| 100 | */ |
| 101 | export function isPathInSandboxWriteAllowlist(resolvedPath: string): boolean { |
| 102 | if (!SandboxManager.isSandboxingEnabled()) { |
| 103 | return false |
| 104 | } |
| 105 | const { allowOnly, denyWithinAllow } = SandboxManager.getFsWriteConfig() |
| 106 | // Resolve symlinks on both sides so comparisons are symmetric (matching |
| 107 | // pathInAllowedWorkingPath). Without this, an allowlist entry that is a |
| 108 | // symlink (e.g. /home/user/proj -> /data/proj) would not match a write to |
| 109 | // its resolved target, causing an unnecessary prompt. Over-conservative, |
| 110 | // not a security issue. All resolved input representations must be allowed |
| 111 | // and none may be denied. Config paths are session-stable, so memoize |
| 112 | // their resolution to avoid N × config.length redundant syscalls per |
| 113 | // command with N write targets (matching getResolvedWorkingDirPaths). |
| 114 | const pathsToCheck = getPathsForPermissionCheck(resolvedPath) |
| 115 | const resolvedAllow = allowOnly.flatMap(getResolvedSandboxConfigPath) as string[] |
| 116 | const resolvedDeny = denyWithinAllow.flatMap(getResolvedSandboxConfigPath) as string[] |
| 117 | return pathsToCheck.every(p => { |
| 118 | for (const denyPath of resolvedDeny) { |
| 119 | if (pathInWorkingPath(p, denyPath)) return false |
| 120 | } |
| 121 | return resolvedAllow.some(allowPath => pathInWorkingPath(p, allowPath)) |
| 122 | }) |
| 123 | } |
| 124 | |
| 125 | // Sandbox config paths are session-stable; memoize their resolved forms to |
| 126 | // avoid repeated lstat/realpath syscalls on every write-target check. |
no test coverage detected