codex-security: _locked_parent locks every path ancestor, so scans under the Windows user profile fail with ERROR_ACCESS_DENIED
<html>
<body>
<!--StartFragment--><h3 dir="ltr" class="mt-3 -mb-1 text-[1.125rem] font-bold">Oggetto</h3>
<div role="group" aria-label="Codice" tabindex="0" class="relative group/copy bg-bg-000/50 border-0.5 border-border-400 rounded-lg focus:outline-none focus-visible:ring-2 focus-visible:ring-accent-100"><div class="sticky opacity-0 group-hover/copy:opacity-100 group-focus-within/copy:opacity-100 top-2 py-2 h-12 w-0 float-right"><div class="absolute right-0 h-8 px-2 items-center inline-flex z-10"></div></div><div class="overflow-x-auto"><pre class="code-block__code !my-0 !rounded-lg !text-sm !leading-relaxed p-3.5" style="color: rgb(20, 24, 31); background: transparent; font-family: var(--font-mono);"><code style="color: rgb(20, 24, 31); background: transparent; font-family: var(--font-mono); white-space: pre-wrap;">codex-security: _locked_parent locks every path ancestor, so scans under the Windows user profile fail with ERROR_ACCESS_DENIED</code></pre></div></div>
<h3 dir="ltr" class="mt-3 -mb-1 text-[1.125rem] font-bold">Testo</h3>
<hr class="border-border-200 border-t-0.5 my-3 mx-1.5">
<h3 dir="ltr" class="mt-3 -mb-1 text-[1.125rem] font-bold">Summary</h3>
<p class="font-claude-response-body break-words whitespace-normal" dir="ltr"><code class="bg-text-200/5 border border-0.5 border-border-300 text-danger-000 whitespace-pre-wrap rounded-[0.4rem] px-1 py-px text-[0.9rem]">codex-security</code> plugin scans fail on Windows whenever the scan directory lives anywhere under <code class="bg-text-200/5 border border-0.5 border-border-300 text-danger-000 whitespace-pre-wrap rounded-[0.4rem] px-1 py-px text-[0.9rem]">C:\Users\<user></code>, which is the default. <code class="bg-text-200/5 border border-0.5 border-border-300 text-danger-000 whitespace-pre-wrap rounded-[0.4rem] px-1 py-px text-[0.9rem]">scripts/windows_scan_local_files.py::_locked_parent</code> opens a directory handle on <strong>every</strong> ancestor of the scan root up to the volume root. Under <code class="bg-text-200/5 border border-0.5 border-border-300 text-danger-000 whitespace-pre-wrap rounded-[0.4rem] px-1 py-px text-[0.9rem]">[windows] sandbox = "elevated"</code> the dedicated sandbox user has no ACL on the interactive user's profile directory, so <code class="bg-text-200/5 border border-0.5 border-border-300 text-danger-000 whitespace-pre-wrap rounded-[0.4rem] px-1 py-px text-[0.9rem]">CreateFileW("C:\Users\<user>")</code> returns <code class="bg-text-200/5 border border-0.5 border-border-300 text-danger-000 whitespace-pre-wrap rounded-[0.4rem] px-1 py-px text-[0.9rem]">ERROR_ACCESS_DENIED</code> (5) and the whole scan-local file operation aborts.</p>
<p class="font-claude-response-body break-words whitespace-normal" dir="ltr">The failure is not fixable by relocating the scan directory: the default scan root is <code class="bg-text-200/5 border border-0.5 border-border-300 text-danger-000 whitespace-pre-wrap rounded-[0.4rem] px-1 py-px text-[0.9rem]">$CODEX_HOME/scans</code> (<code class="bg-text-200/5 border border-0.5 border-border-300 text-danger-000 whitespace-pre-wrap rounded-[0.4rem] px-1 py-px text-[0.9rem]">deep_scan_workbench.py:759</code> → <code class="bg-text-200/5 border border-0.5 border-border-300 text-danger-000 whitespace-pre-wrap rounded-[0.4rem] px-1 py-px text-[0.9rem]">state_dir()</code>), i.e. <code class="bg-text-200/5 border border-0.5 border-border-300 text-danger-000 whitespace-pre-wrap rounded-[0.4rem] px-1 py-px text-[0.9rem]">C:\Users\<user>\.codex\...</code>, and the workspace itself is normally under the profile too. Every candidate path traverses the denied ancestor.</p>
<h3 dir="ltr" class="mt-3 -mb-1 text-[1.125rem] font-bold">Environment</h3>
<ul dir="ltr" class="[li_&]:mb-0 [li_&]:mt-1 [li_&]:gap-1 [&:not(:last-child)_ul]:pb-1 [&:not(:last-child)_ol]:pb-1 list-disc flex flex-col gap-1 pl-8 mb-3 print:block print:space-y-1">
<li class="font-claude-response-body whitespace-normal break-words pl-2">Codex desktop app, Windows (x64)</li>
<li class="font-claude-response-body whitespace-normal break-words pl-2"><code class="bg-text-200/5 border border-0.5 border-border-300 text-danger-000 whitespace-pre-wrap rounded-[0.4rem] px-1 py-px text-[0.9rem]">codex-security</code> plugin <code class="bg-text-200/5 border border-0.5 border-border-300 text-danger-000 whitespace-pre-wrap rounded-[0.4rem] px-1 py-px text-[0.9rem]">0.1.19</code> (<code class="bg-text-200/5 border border-0.5 border-border-300 text-danger-000 whitespace-pre-wrap rounded-[0.4rem] px-1 py-px text-[0.9rem]">openai-curated-remote</code>)</li>
<li class="font-claude-response-body whitespace-normal break-words pl-2"><code class="bg-text-200/5 border border-0.5 border-border-300 text-danger-000 whitespace-pre-wrap rounded-[0.4rem] px-1 py-px text-[0.9rem]">~/.codex/config.toml</code>: <code class="bg-text-200/5 border border-0.5 border-border-300 text-danger-000 whitespace-pre-wrap rounded-[0.4rem] px-1 py-px text-[0.9rem]">[windows] sandbox = "elevated"</code></li>
<li class="font-claude-response-body whitespace-normal break-words pl-2">Python 3.12 (per <code class="bg-text-200/5 border border-0.5 border-border-300 text-danger-000 whitespace-pre-wrap rounded-[0.4rem] px-1 py-px text-[0.9rem]">scripts/__pycache__/*.cpython-312.pyc</code>)</li>
</ul>
<h3 dir="ltr" class="mt-3 -mb-1 text-[1.125rem] font-bold">Root cause</h3>
<p class="font-claude-response-body break-words whitespace-normal" dir="ltr"><code class="bg-text-200/5 border border-0.5 border-border-300 text-danger-000 whitespace-pre-wrap rounded-[0.4rem] px-1 py-px text-[0.9rem]">scripts/windows_scan_local_files.py</code>, lines 409–415:</p>
<div role="group" aria-label="Codice python" tabindex="0" class="relative group/copy bg-bg-000/50 border-0.5 border-border-400 rounded-lg focus:outline-none focus-visible:ring-2 focus-visible:ring-accent-100"><div class="sticky opacity-0 group-hover/copy:opacity-100 group-focus-within/copy:opacity-100 top-2 py-2 h-12 w-0 float-right"><div class="absolute right-0 h-8 px-2 items-center inline-flex z-10"></div></div><div class="text-text-500 font-small p-3.5 pb-0">python</div><div class="overflow-x-auto"><pre class="code-block__code !my-0 !rounded-lg !text-sm !leading-relaxed p-3.5" style="color: rgb(20, 24, 31); background: transparent; font-family: var(--font-mono);"><code class="language-python" style="color: rgb(20, 24, 31); background: transparent; font-family: var(--font-mono); white-space: pre;"> <span style="color: rgb(110, 118, 135);"># Absolute-path Win32 calls remain safe only while every ancestor is</span>
<span style="color: rgb(110, 118, 135);"># fixed in place. Otherwise an attacker could rename an ancestor of the</span>
<span style="color: rgb(110, 118, 135);"># scan root and substitute a different tree at the stored path.</span>
<span style="color: rgb(129, 0, 194);">for</span> directory_path <span style="color: rgb(129, 0, 194);">in</span> <span style="color: rgb(43, 48, 59);">(</span>*<span style="color: rgb(0, 81, 194);">reversed</span><span style="color: rgb(43, 48, 59);">(</span>root_path<span style="color: rgb(43, 48, 59);">.</span>parents<span style="color: rgb(43, 48, 59);">),</span> root_path<span style="color: rgb(43, 48, 59);">):</span>
directory_handle = _open_directory<span style="color: rgb(43, 48, 59);">(</span>directory_path<span style="color: rgb(43, 48, 59);">)</span>
<span style="color: rgb(129, 0, 194);">assert</span> directory_handle <span style="color: rgb(129, 0, 194);">is</span> <span style="color: rgb(129, 0, 194);">not</span> <span style="color: rgb(0, 128, 128);">None</span>
handles<span style="color: rgb(43, 48, 59);">.</span>append<span style="color: rgb(43, 48, 59);">(</span>directory_handle<span style="color: rgb(43, 48, 59);">)</span></code></pre></div></div>
<p class="font-claude-response-body break-words whitespace-normal" dir="ltr"><code class="bg-text-200/5 border border-0.5 border-border-300 text-danger-000 whitespace-pre-wrap rounded-[0.4rem] px-1 py-px text-[0.9rem]">_open_directory</code> → <code class="bg-text-200/5 border border-0.5 border-border-300 text-danger-000 whitespace-pre-wrap rounded-[0.4rem] px-1 py-px text-[0.9rem]">_create_file</code> with <code class="bg-text-200/5 border border-0.5 border-border-300 text-danger-000 whitespace-pre-wrap rounded-[0.4rem] px-1 py-px text-[0.9rem]">FILE_READ_ATTRIBUTES | FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT</code>. <code class="bg-text-200/5 border border-0.5 border-border-300 text-danger-000 whitespace-pre-wrap rounded-[0.4rem] px-1 py-px text-[0.9rem]">_create_file</code> only tolerates <code class="bg-text-200/5 border border-0.5 border-border-300 text-danger-000 whitespace-pre-wrap rounded-[0.4rem] px-1 py-px text-[0.9rem]">ERROR_FILE_NOT_FOUND</code> / <code class="bg-text-200/5 border border-0.5 border-border-300 text-danger-000 whitespace-pre-wrap rounded-[0.4rem] px-1 py-px text-[0.9rem]">ERROR_PATH_NOT_FOUND</code> (and only when <code class="bg-text-200/5 border border-0.5 border-border-300 text-danger-000 whitespace-pre-wrap rounded-[0.4rem] px-1 py-px text-[0.9rem]">missing_ok=True</code>); every other failure goes to <code class="bg-text-200/5 border border-0.5 border-border-300 text-danger-000 whitespace-pre-wrap rounded-[0.4rem] px-1 py-px text-[0.9rem]">_raise_last_error</code>.</p>
<p class="font-claude-response-body break-words whitespace-normal" dir="ltr">The walked chain for a default scan root is:</p>
<div role="group" aria-label="Codice" tabindex="0" class="relative group/copy bg-bg-000/50 border-0.5 border-border-400 rounded-lg focus:outline-none focus-visible:ring-2 focus-visible:ring-accent-100"><div class="sticky opacity-0 group-hover/copy:opacity-100 group-focus-within/copy:opacity-100 top-2 py-2 h-12 w-0 float-right"><div class="absolute right-0 h-8 px-2 items-center inline-flex z-10"></div></div><div class="overflow-x-auto"><pre class="code-block__code !my-0 !rounded-lg !text-sm !leading-relaxed p-3.5" style="color: rgb(20, 24, 31); background: transparent; font-family: var(--font-mono);"><code style="color: rgb(20, 24, 31); background: transparent; font-family: var(--font-mono); white-space: pre-wrap;">C:\ → C:\Users → C:\Users\<user> → C:\Users\<user>\.codex → ... → <scan root>
^^^^^^^^^^^^^^^^ ERROR_ACCESS_DENIED under the elevated sandbox</code></pre></div></div>
<p class="font-claude-response-body break-words whitespace-normal" dir="ltr">Note that the deeper, explicitly-granted directories open fine — Windows bypass-traverse-checking (<code class="bg-text-200/5 border border-0.5 border-border-300 text-danger-000 whitespace-pre-wrap rounded-[0.4rem] px-1 py-px text-[0.9rem]">SeChangeNotifyPrivilege</code>, granted to <code class="bg-text-200/5 border border-0.5 border-border-300 text-danger-000 whitespace-pre-wrap rounded-[0.4rem] px-1 py-px text-[0.9rem]">Everyone</code> by default) means an absolute open of a descendant does <strong>not</strong> require rights on that ancestor. Only the plugin's explicit per-ancestor open fails.</p>
<h3 dir="ltr" class="mt-3 -mb-1 text-[1.125rem] font-bold">Why the current behaviour buys nothing</h3>
<p class="font-claude-response-body break-words whitespace-normal" dir="ltr">The stated goal is to prevent an attacker from renaming an ancestor of the scan root mid-operation. But a directory this process cannot <strong>open</strong> is also a directory it cannot <strong>lock</strong>. Hard-failing on <code class="bg-text-200/5 border border-0.5 border-border-300 text-danger-000 whitespace-pre-wrap rounded-[0.4rem] px-1 py-px text-[0.9rem]">ERROR_ACCESS_DENIED</code> therefore adds no guarantee over skipping it — it only makes the scan impossible on the default install layout. An attacker who can rename <code class="bg-text-200/5 border border-0.5 border-border-300 text-danger-000 whitespace-pre-wrap rounded-[0.4rem] px-1 py-px text-[0.9rem]">C:\Users\<user></code> already owns the interactive account, which is strictly outside the scanner's threat model.</p>
<h3 dir="ltr" class="mt-3 -mb-1 text-[1.125rem] font-bold">Proposed fix</h3>
<p class="font-claude-response-body break-words whitespace-normal" dir="ltr">Tolerate <code class="bg-text-200/5 border border-0.5 border-border-300 text-danger-000 whitespace-pre-wrap rounded-[0.4rem] px-1 py-px text-[0.9rem]">ERROR_ACCESS_DENIED</code> for <strong>ancestors only</strong>, and keep the scan root itself mandatory. All existing guarantees are preserved: reparse points still fail hard anywhere, <code class="bg-text-200/5 border border-0.5 border-border-300 text-danger-000 whitespace-pre-wrap rounded-[0.4rem] px-1 py-px text-[0.9rem]">_verify_directory</code> / <code class="bg-text-200/5 border border-0.5 border-border-300 text-danger-000 whitespace-pre-wrap rounded-[0.4rem] px-1 py-px text-[0.9rem]">_verify_handle_path</code> still run on every handle actually obtained, the <code class="bg-text-200/5 border border-0.5 border-border-300 text-danger-000 whitespace-pre-wrap rounded-[0.4rem] px-1 py-px text-[0.9rem]">st_dev</code>/<code class="bg-text-200/5 border border-0.5 border-border-300 text-danger-000 whitespace-pre-wrap rounded-[0.4rem] px-1 py-px text-[0.9rem]">st_ino</code> identity re-check on the scan root is untouched, and any denied directory <em>at or below</em> the scan root still fails hard.</p>
<div role="group" aria-label="Codice diff" tabindex="0" class="relative group/copy bg-bg-000/50 border-0.5 border-border-400 rounded-lg focus:outline-none focus-visible:ring-2 focus-visible:ring-accent-100"><div class="sticky opacity-0 group-hover/copy:opacity-100 group-focus-within/copy:opacity-100 top-2 py-2 h-12 w-0 float-right"><div class="absolute right-0 h-8 px-2 items-center inline-flex z-10"></div></div><div class="text-text-500 font-small p-3.5 pb-0">diff</div><div class="overflow-x-auto"><pre class="code-block__code !my-0 !rounded-lg !text-sm !leading-relaxed p-3.5" style="color: rgb(20, 24, 31); background: transparent; font-family: var(--font-mono);"><code class="language-diff" style="color: rgb(20, 24, 31); background: transparent; font-family: var(--font-mono); white-space: pre;"><span style="color: rgb(43, 48, 59);">---</span> a/scripts/windows_scan_local_files.py
<span style="color: rgb(43, 48, 59);">+++</span> b/scripts/windows_scan_local_files.py
<span style="color: rgb(43, 48, 59);">@@</span> -63,10 +63,12 <span style="color: rgb(43, 48, 59);">@@</span>
_FILE_NAME_OPENED = 0x00000008
_ERROR_FILE_NOT_FOUND = 2
_ERROR_PATH_NOT_FOUND = 3
<span style="color: rgb(43, 48, 59);">+</span><span style="color: rgb(0, 128, 0);">_ERROR_ACCESS_DENIED = 5</span>
_ERROR_FILE_EXISTS = 80
_ERROR_ALREADY_EXISTS = 183
_MISSING_ERRORS = {_ERROR_FILE_NOT_FOUND, _ERROR_PATH_NOT_FOUND}
_COLLISION_ERRORS = {_ERROR_FILE_EXISTS, _ERROR_ALREADY_EXISTS}
<span style="color: rgb(43, 48, 59);">+</span><span style="color: rgb(0, 128, 0);">_DENIED_ERRORS = {_ERROR_ACCESS_DENIED}</span>
_INVALID_HANDLE_VALUE = ctypes.c_void_p(-1).value
_MAX_WRITE_CHUNK = 1024 * 1024
<span style="color: rgb(43, 48, 59);">@@</span> -285,6 +287,7 <span style="color: rgb(43, 48, 59);">@@</span>
disposition: int,
flags: int,
missing_ok: bool = False,
<span style="color: rgb(43, 48, 59);">+</span><span style="color: rgb(0, 128, 0);"> denied_ok: bool = False,</span>
) -> _OwnedHandle | None:
_require_windows()
handle = _CreateFileW(
<span style="color: rgb(43, 48, 59);">@@</span> -300,6 +303,8 <span style="color: rgb(43, 48, 59);">@@</span>
error = ctypes.get_last_error()
if missing_ok and error in _MISSING_ERRORS:
return None
<span style="color: rgb(43, 48, 59);">+</span><span style="color: rgb(0, 128, 0);"> if denied_ok and error in _DENIED_ERRORS:</span>
<span style="color: rgb(43, 48, 59);">+</span><span style="color: rgb(0, 128, 0);"> return None</span>
_raise_last_error("CreateFileW", path)
return _OwnedHandle(int(handle))
<span style="color: rgb(43, 48, 59);">@@</span> -365,7 +370,12 <span style="color: rgb(43, 48, 59);">@@</span>
return canonical, (expected.st_dev, expected.st_ino)
<span style="color: rgb(43, 48, 59);">-</span><span style="color: rgb(184, 10, 24);">def _open_directory(path: Path, *, missing_ok: bool = False) -> _OwnedHandle | None:</span>
<span style="color: rgb(43, 48, 59);">+</span><span style="color: rgb(0, 128, 0);">def _open_directory(</span>
<span style="color: rgb(43, 48, 59);">+</span><span style="color: rgb(0, 128, 0);"> path: Path,</span>
<span style="color: rgb(43, 48, 59);">+</span><span style="color: rgb(0, 128, 0);"> *,</span>
<span style="color: rgb(43, 48, 59);">+</span><span style="color: rgb(0, 128, 0);"> missing_ok: bool = False,</span>
<span style="color: rgb(43, 48, 59);">+</span><span style="color: rgb(0, 128, 0);"> denied_ok: bool = False,</span>
<span style="color: rgb(43, 48, 59);">+</span><span style="color: rgb(0, 128, 0);">) -> _OwnedHandle | None:</span>
handle = _create_file(
path,
access=_FILE_READ_ATTRIBUTES,
<span style="color: rgb(43, 48, 59);">@@</span> -373,6 +383,7 <span style="color: rgb(43, 48, 59);">@@</span>
disposition=_OPEN_EXISTING,
flags=_FILE_FLAG_BACKUP_SEMANTICS | _FILE_FLAG_OPEN_REPARSE_POINT,
missing_ok=missing_ok,
<span style="color: rgb(43, 48, 59);">+</span><span style="color: rgb(0, 128, 0);"> denied_ok=denied_ok,</span>
)
if handle is not None:
try:
<span style="color: rgb(43, 48, 59);">@@</span> -409,10 +420,30 <span style="color: rgb(43, 48, 59);">@@</span>
# Absolute-path Win32 calls remain safe only while every ancestor is
# fixed in place. Otherwise an attacker could rename an ancestor of the
# scan root and substitute a different tree at the stored path.
<span style="color: rgb(43, 48, 59);">-</span><span style="color: rgb(184, 10, 24);"> for directory_path in (*reversed(root_path.parents), root_path):</span>
<span style="color: rgb(43, 48, 59);">-</span><span style="color: rgb(184, 10, 24);"> directory_handle = _open_directory(directory_path)</span>
<span style="color: rgb(43, 48, 59);">-</span><span style="color: rgb(184, 10, 24);"> assert directory_handle is not None</span>
<span style="color: rgb(43, 48, 59);">+</span><span style="color: rgb(0, 128, 0);"> #</span>
<span style="color: rgb(43, 48, 59);">+</span><span style="color: rgb(0, 128, 0);"> # Ancestors above the trusted scan root are frequently outside the</span>
<span style="color: rgb(43, 48, 59);">+</span><span style="color: rgb(0, 128, 0);"> # process's granted read set. Under the Codex Windows sandbox the</span>
<span style="color: rgb(43, 48, 59);">+</span><span style="color: rgb(0, 128, 0);"> # dedicated sandbox user has no ACL on the interactive user's profile</span>
<span style="color: rgb(43, 48, 59);">+</span><span style="color: rgb(0, 128, 0);"> # directory, so CreateFileW on C:\Users\<name> fails with</span>
<span style="color: rgb(43, 48, 59);">+</span><span style="color: rgb(0, 128, 0);"> # ERROR_ACCESS_DENIED even though every path under the granted scan</span>
<span style="color: rgb(43, 48, 59);">+</span><span style="color: rgb(0, 128, 0);"> # root opens normally (Windows bypass-traverse-checking makes the</span>
<span style="color: rgb(43, 48, 59);">+</span><span style="color: rgb(0, 128, 0);"> # absolute open succeed without rights on that ancestor).</span>
<span style="color: rgb(43, 48, 59);">+</span><span style="color: rgb(0, 128, 0);"> #</span>
<span style="color: rgb(43, 48, 59);">+</span><span style="color: rgb(0, 128, 0);"> # A directory this process cannot open is also a directory it cannot</span>
<span style="color: rgb(43, 48, 59);">+</span><span style="color: rgb(0, 128, 0);"> # lock, under any configuration. Failing the whole operation there buys</span>
<span style="color: rgb(43, 48, 59);">+</span><span style="color: rgb(0, 128, 0);"> # no additional guarantee and blocks every scan whose root lives under</span>
<span style="color: rgb(43, 48, 59);">+</span><span style="color: rgb(0, 128, 0);"> # the user profile. Skip an ancestor that denies access and keep</span>
<span style="color: rgb(43, 48, 59);">+</span><span style="color: rgb(0, 128, 0);"> # locking the reachable ones; a reparse point or an identity mismatch</span>
<span style="color: rgb(43, 48, 59);">+</span><span style="color: rgb(0, 128, 0);"> # anywhere still fails hard, and the scan root itself must always be</span>
<span style="color: rgb(43, 48, 59);">+</span><span style="color: rgb(0, 128, 0);"> # lockable.</span>
<span style="color: rgb(43, 48, 59);">+</span><span style="color: rgb(0, 128, 0);"> for directory_path in reversed(root_path.parents):</span>
<span style="color: rgb(43, 48, 59);">+</span><span style="color: rgb(0, 128, 0);"> directory_handle = _open_directory(directory_path, denied_ok=True)</span>
<span style="color: rgb(43, 48, 59);">+</span><span style="color: rgb(0, 128, 0);"> if directory_handle is None:</span>
<span style="color: rgb(43, 48, 59);">+</span><span style="color: rgb(0, 128, 0);"> continue</span>
handles.append(directory_handle)
<span style="color: rgb(43, 48, 59);">+</span><span style="color: rgb(0, 128, 0);"> root_handle = _open_directory(root_path)</span>
<span style="color: rgb(43, 48, 59);">+</span><span style="color: rgb(0, 128, 0);"> assert root_handle is not None</span>
<span style="color: rgb(43, 48, 59);">+</span><span style="color: rgb(0, 128, 0);"> handles.append(root_handle)</span>
current_root = root_path.lstat()
if (current_root.st_dev, current_root.st_ino) != expected_root_identity:
raise _invalid_path(scan_dir, "scan directory changed while it was being opened")</code></pre></div></div>
<h3 dir="ltr" class="mt-3 -mb-1 text-[1.125rem] font-bold">Test</h3>
<p class="font-claude-response-body break-words whitespace-normal" dir="ltr">A standalone regression test is attached (<code class="bg-text-200/5 border border-0.5 border-border-300 text-danger-000 whitespace-pre-wrap rounded-[0.4rem] px-1 py-px text-[0.9rem]">test_locked_parent_ancestors.py</code>). It stubs the Win32 layer so it runs on any platform and asserts:</p>
<div dir="ltr" class="overflow-x-auto w-full pl-[var(--msg-block-inset,0.5rem)] pr-2 mb-6 print:overflow-x-visible">
| Case | Expected
-- | -- | --
1 | No denial | every ancestor and the root are locked (unchanged behaviour)
2 | Profile ancestor denied | denied ancestor skipped, operation completes, reachable ancestors below it still locked
3 | Multiple ancestors denied | all skipped, operation completes
4 | Scan root itself denied | still raises WindowsScanLocalFileError(errno=5)
5 | Child under the scan root denied | still raises WindowsScanLocalFileError(errno=5)
</div>
<p class="font-claude-response-body break-words whitespace-normal" dir="ltr">Result: 7/7 assertions pass on the patched module. Against the unpatched module the run aborts at case 2 with</p>
<div role="group" aria-label="Codice" tabindex="0" class="relative group/copy bg-bg-000/50 border-0.5 border-border-400 rounded-lg focus:outline-none focus-visible:ring-2 focus-visible:ring-accent-100"><div class="sticky opacity-0 group-hover/copy:opacity-100 group-focus-within/copy:opacity-100 top-2 py-2 h-12 w-0 float-right"><div class="absolute right-0 h-8 px-2 items-center inline-flex z-10"></div></div><div class="overflow-x-auto"><pre class="code-block__code !my-0 !rounded-lg !text-sm !leading-relaxed p-3.5" style="color: rgb(20, 24, 31); background: transparent; font-family: var(--font-mono);"><code style="color: rgb(20, 24, 31); background: transparent; font-family: var(--font-mono); white-space: pre-wrap;">wslf.WindowsScanLocalFileError: [Errno 5] CreateFileW: Access is denied: '/u/profile'</code></pre></div></div>
<p class="font-claude-response-body break-words whitespace-normal" dir="ltr">which is the reported failure.</p>
<h3 dir="ltr" class="mt-3 -mb-1 text-[1.125rem] font-bold">Alternatives considered</h3>
<ul dir="ltr" class="[li_&]:mb-0 [li_&]:mt-1 [li_&]:gap-1 [&:not(:last-child)_ul]:pb-1 [&:not(:last-child)_ol]:pb-1 list-disc flex flex-col gap-1 pl-8 mb-3 print:block print:space-y-1">
<li class="font-claude-response-body whitespace-normal break-words pl-2"><strong><code class="bg-text-200/5 border border-0.5 border-border-300 text-danger-000 whitespace-pre-wrap rounded-[0.4rem] px-1 py-px text-[0.9rem]">/sandbox-add-read-dir C:\Users\<user></code></strong> — works, but grants the sandbox user read access to the entire user profile (<code class="bg-text-200/5 border border-0.5 border-border-300 text-danger-000 whitespace-pre-wrap rounded-[0.4rem] px-1 py-px text-[0.9rem]">.ssh</code>, <code class="bg-text-200/5 border border-0.5 border-border-300 text-danger-000 whitespace-pre-wrap rounded-[0.4rem] px-1 py-px text-[0.9rem]">.aws</code>, <code class="bg-text-200/5 border border-0.5 border-border-300 text-danger-000 whitespace-pre-wrap rounded-[0.4rem] px-1 py-px text-[0.9rem]">.codex\auth.json</code>) for the session. A poor trade when the whole point of the process is scanning potentially hostile repository content.</li>
<li class="font-claude-response-body whitespace-normal break-words pl-2"><strong>Relocating the scan root off the profile</strong> (e.g. <code class="bg-text-200/5 border border-0.5 border-border-300 text-danger-000 whitespace-pre-wrap rounded-[0.4rem] px-1 py-px text-[0.9rem]">--scan-root C:\codex-scans</code>) — avoids the denied ancestor, but the flag is not reachable from normal desktop usage and the default remains broken.</li>
<li class="font-claude-response-body whitespace-normal break-words pl-2"><strong>Stopping the walk unconditionally at the scan root</strong> — simpler, but discards ancestor locking even where it is achievable. The patch above is strictly stronger.</li>
</ul>
<h3 dir="ltr" class="mt-3 -mb-1 text-[1.125rem] font-bold">Notes</h3>
<ul dir="ltr" class="[li_&]:mb-0 [li_&]:mt-1 [li_&]:gap-1 [&:not(:last-child)_ul]:pb-1 [&:not(:last-child)_ol]:pb-1 list-disc flex flex-col gap-1 pl-8 mb-3 print:block print:space-y-1">
<li class="font-claude-response-body whitespace-normal break-words pl-2">Not verified on a live Windows host: the attached test stubs <code class="bg-text-200/5 border border-0.5 border-border-300 text-danger-000 whitespace-pre-wrap rounded-[0.4rem] px-1 py-px text-[0.9rem]">CreateFileW</code> and the sandbox ACLs. The Win32 error code was inferred from the sandbox ACL model, not captured from a debugger. If the observed code is not <code class="bg-text-200/5 border border-0.5 border-border-300 text-danger-000 whitespace-pre-wrap rounded-[0.4rem] px-1 py-px text-[0.9rem]">5</code>, extending <code class="bg-text-200/5 border border-0.5 border-border-300 text-danger-000 whitespace-pre-wrap rounded-[0.4rem] px-1 py-px text-[0.9rem]">_DENIED_ERRORS</code> should be sufficient.</li>
<li class="font-claude-response-body whitespace-normal break-words pl-2">The plugin ships under <code class="bg-text-200/5 border border-0.5 border-border-300 text-danger-000 whitespace-pre-wrap rounded-[0.4rem] px-1 py-px text-[0.9rem]">plugins\cache\...</code>, so a local patch is overwritten on the next plugin update.</li></ul><!--EndFragment-->
</body>
</html>
1 Comment
Confirmed on a live Windows host that this remains reproducible in
codex-securityplugin0.1.21.Environment:
26.818.5229.0(x64)10.0.26220.0(x64)0.147.0codex-securityplugin0.1.21fromopenai-curated-remoteThe terminal diff finalizer failed while reading
scan-manifest.json:I isolated the behavior with direct, read-only calls to the plugin's
_open_directoryhelper:C:\Users\<user>failed inside the sandbox withWindowsScanLocalFileError: [Errno 5] ... Access is denied.C:\Users\<user>directory-handle probe succeeded outside the sandbox.Quoting the paths, using forward slashes, and relocating the source through a no-space junction did not change the failure. That is consistent with
_locked_parentwalking the scan directory's ancestors; the source-root spelling is not the cause.As a bounded recovery, the same pinned finalizer command completed successfully outside the sandbox. It sealed the manifest, verified the intended Git revisions and snapshot digest, and generated
report.mdplus SARIF. No plugin files were patched.This provides live-host confirmation for the issue's diagnosis and shows that a narrowly scoped unsandboxed finalization path is a viable temporary workaround. A product fix should prevent normal Windows scan finalization from requiring users to grant the sandbox access to their entire profile or manually escape the sandbox.