Skip to content

fix(devx): the four symlink-blind entry guards outside scripts/, where check:entry-guard cannot see them - #10755

Merged
os-zhuang merged 3 commits into
mainfrom
claude/issue-10269-symlink-blind-entry-guards
Aug 21, 2026
Merged

fix(devx): the four symlink-blind entry guards outside scripts/, where check:entry-guard cannot see them#10755
os-zhuang merged 3 commits into
mainfrom
claude/issue-10269-symlink-blind-entry-guards

Conversation

@claude

@claudeclaudeBot commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Closes#10269

The four process.argv[1] entry guards that live outside scripts/**, where
#10086's class-closing check:entry-guard deliberately does not scan. Both
spellings are on that card's measured-broken list, and both failure directions
are reproduced on the real files below before being fixed.

siteold spellingfamily
packages/objectql/scripts/dry-run-hash-compat.tsprocess.argv[1] && /dry-run-hash-compat\.ts$/.test(process.argv[1])regex-basename (S3/S10)
packages/core/examples/kernel-features-example.tsimport.meta.url === `file://${process.argv[1]}` template (S11)
packages/core/examples/phase2-integration.tssametemplate (S11)
examples/embed-objectql/src/index.tssametemplate (S11)

All four now carry the same two-leg predicate packages/cli/src/utils/invocation.ts
(isProcessEntry) and scripts/invoked-as.mjs (invokedAs) already use: a
resolve fast path, a realpath leg for symlinked checkouts, and the directory candidates for a
node invocation that names a directory. Comparing resolved paths rather than URL strings is what
keeps percent-encoding out of the answer entirely.

Why four inline spellings and not one shared helper

The brief's first hazard was that reaching for scripts/invoked-as.mjs from
packages/** would create a package → scripts/ dependency. Measured,
rather than assumed — and the conclusion is stronger than "probably not allowed":
no package that all three consumers can legally reach could host this predicate.

  • @objectstack/cli, the home of isProcessEntry, depends on both
    @objectstack/core and @objectstack/objectql
    . Importing it from either would
    be a workspace dependency cycle. It also declares no exports map at all
    and packs files: ["dist"], so src/utils/invocation.ts is not reachable by
    subpath from anywhere outside the package.
  • examples/embed-objectql does not depend on @objectstack/cli either, and adding
    it would pull a 40-dependency CLI into an example whose entire stated purpose
    (ADR-0076) is "no kernel, no plugins, no metadata protocol".
  • The intersection of the dependency sets of @objectstack/objectql,
    @objectstack/core and @objectstack/example-embed-objectql is exactly one
    package: @objectstack/spec. Its runtime dependencies are zod and
    pg-connection-string, it has no node: import in any non-test source, and
    ADR-0076 D5 ratifies that shape — a node:fs / node:url predicate cannot live
    there.
  • Root scripts/invoked-as.mjs's own header states the non-import rule and its
    reason. There is precedent for packages/*/scripts/*.ts importing root
    scripts/ (packages/spec/scripts/build-docs.ts), but it does not transfer here:
    dry-run-hash-compat.ts is imported by packages/objectql/src/dry-run-hash-compat.test.ts,
    so the edge would put a root-scripts/ module into a package test's input
    graph — invisible to turbo ls --affected and to the test task's input hashing.
    That is precisely the class check:cross-package-test-inputs exists for; its own
    self-test pins import ... from '../../../../scripts/js-comment-mask.mjs' as a
    flagged escape.

So the duplication is structural, the same way invoked-as.mjs and invocation.ts
are already deliberate duplicates of each other. Each copy is byte-identical and
carries the divergence rule in its comment: change one, change all of them.
Widening check:entry-guard's population is left as a proposal (see the issue
report), not done here.

Measurements

The old spellings are broken — reproduced on the real files

examples/embed-objectql/src/index.ts, copied verbatim to a real directory whose
name contains #, with the package's own node_modules reachable:

control old template guard, ordinary path exit=0 750 bytes (prints "Active accounts (2): ...")
1. old template guard, parent dir "a#b" exit=0 0 bytes <-- INERT, no symlink involved
2. new guard, parent dir "a#b" exit=0 750 bytes
3. old template guard, via a symlink exit=0 0 bytes <-- INERT
4. new guard, via a symlink exit=0 750 bytes

packages/objectql/scripts/dry-run-hash-compat.ts reached through a symlink that
renames it, on a snapshot the direct run calls INCOMPATIBLE:

before: exit=0, 0 bytes <-- a caller holding result.status reads a PASS
after: exit=1, 1044 bytes <-- byte-identical to the direct run

That is the silent-success direction the card is about, on a compatibility checker.

The fire-on-import direction, also on the real file: an unrelated entry script
whose own path merely ends in dry-run-hash-compat.ts, importing runDryRun:

before: exit=2, "Usage: pnpm tsx ..." <-- the tool's CLI ran INSIDE the importer's
process and killed it mid-import; the
importer's own code never ran
after: exit=0, "IMPORTER ALIVE, rows= 0"

A note on family assignment: the regex-basename form turns out to be immune to
the percent-encoding failure (it never builds a URL), and the template form is immune
to the fire-on-import failure. The card's split of the two shapes into two families is
confirmed rather than merely restated.

The CLI is unchanged

dry-run-hash-compat.ts's three invocations, before and after, byte-for-byte
identical output and identical exit codes
:

snapshot compatible exit=0 522 bytes IDENTICAL
snapshot incompatible exit=1 1044 bytes IDENTICAL
no argument (usage) exit=2 81 bytes IDENTICAL

Hazard 2 — check:examples-live-imports reconciliation

invisibleinputs-declaredgraph-visible
before (origin/main8f04d9a)0 (declared)670
after (ca022387f2)0 (declared)670

No count moved, and the reason is structural rather than lucky. That gate
inventories packages/**tests that reach into examples/**; its tiers classify
couplings by what CI can see of that direction. This change adds only node: builtin
imports to an example's own source — it creates no coupling from a packages/** test
into examples/**, and the gate's self-test explicitly does not flag node:
builtins. Measured both ways rather than assumed.

Rider: two stale facts in packages/cli/src/utils/invocation.ts's header

Both are comment-only, both were called out to ride along with this card.

  1. The header said The invokedDirectly entry guard is spelled ~8 ways across scripts/, and every one of them makes its script silently inert (exit 0, no output) when reached through a symlink — including check-governed-merges.mjs, the governed-surface register #10086 measured the guard "in ~8 spellings". The census is
    eleven distinct spellings across 33 files, nine of them wrong; ~8 was the
    estimate in that card's body, not the measurement.
  2. Bounded in-place fix. The same header cited
    invocation.cli-name-parity.test.ts as the pin keeping INVOCATION_PREFIX equal
    to CLI_NAME. No such file is in the tree — the assertion is real but lives at
    invocation.test.ts:210. A reader grepping for the cited filename finds nothing
    and could read the parity as unguarded. Same file, same class (a stale factual
    claim in the same module docstring), mechanically pinned by the existing case, no
    new verification surface.

Changeset

skip-changeset, and the measurement behind it: none of the four guard files
ship
. packages/objectql and packages/core both pack files: ["dist"], and their
tsup entries are src/index.ts + src/core.ts and src/index.ts + src/logger.ts
respectively — packages/objectql/scripts/** and packages/core/examples/** are
neither packed nor bundled. examples/embed-objectql is "private": true. The one
published file touched, packages/cli/src/utils/invocation.ts, is comment-only
(every changed line in that file's diff begins with *). No published runtime
behaviour changes, so this is not the PR #10697 situation.

Verification

Gate union re-run after the final commit, at ca022387f2. node scripts/pm/dispatch-gates.mjs
with no paths named six families; all six pass, plus six more run because they were
reasoned about rather than named (the derivation reports check:examples-live-imports
and check:parse-guard as unreachable-by-construction, so it can never name them):

error-code-casing exit=0 no lowercase error codes in 4354 scanned file(s)
slot-lookup exit=0 baseline key set verified against 8f04d9a: no files added
test-source-alias exit=0 72 packages with tests scanned
type-source-resolution exit=0 76 packages with a tsconfig.json scanned
engine-split-ratio exit=0 ADR-0076 D7 ratio 97.5%
affected-docs exit=0 308 self-test cases pass
examples-live-imports exit=0 0 invisible (declared), 6 inputs-declared, 70 graph-visible
entry-guard exit=0 125 scripts/ file(s) - every entry guard goes through invoked-as.mjs
parse-guard exit=0 124 scripts/ file(s) - every TypeScript parse goes through ts-parse.mjs
nul-bytes exit=0 6231 text file(s), no raw ASCII control bytes
cross-package-test-inputs exit=0 13 package(s) read outside themselves, all declared
driver-memory-census exit=0 every declaration is ledgered, every ledger entry is live

Tests:

@objectstack/objectql 225 files, 3983 tests passed
@objectstack/example-embed-objectql 1 file, 2 tests passed + typecheck clean
@objectstack/objectql typecheck clean
packages/cli invocation.test.ts 1 file, 17 tests passed

⚠️ Two honest limits on the local run, both of which CI closes:

  • packages/cli's full suite is not a clean measurement in this worktree — 66 of
    139 files fail with Cannot find module .../@objectstack/client/dist/index.mjs,
    i.e. the cli dependency closure is unbuilt here (I built only objectql's and the
    embed example's). The change to that package is comment-only, and the file's own
    pin (invocation.test.ts, 17/17) passes.
  • @objectstack/core has no typecheck script, and the two files under
    packages/core/examples/ sit outside every tsconfig (packages/core's
    include is ["src/**/*"]). They are verified by running them under tsx,
    which is the stronger evidence for an entry guard anyway. Filed as a finding.
    CORRECTED at 4d77499e10: this bullet originally counted
    packages/objectql/scripts/dry-run-hash-compat.ts as a third such file. It is
    outside packages/objectql's owntsc -p, but it is not outside every
    tsc program -- src/dry-run-hash-compat.test.ts imports it and the TEST_DEBT
    re-measure type-checks that test, which is exactly how this PR went red.

Repair at 4d77499e10 — the guard's ESM seed is a compile error in this package

ca022387f2 was RED on Type Check · debt ledger, and only there:
@objectstack/objectql re-measured 356 raw tsc errors against a frozen
TEST_DEBT of 355. One error, and it was the guard's one novel construct.

packages/objectql/package.json declares no "type", so under the repo-wide
module: NodeNext every file in that package has CommonJS format — and
import.meta in a CommonJS-format file is a hard TS1470. The other four sites all
live in packages whose package.json says "type": "module", which is why exactly
one package drifted.

Two things hid it from the local run that shipped ca022387f2:

  1. packages/objectql/tsconfig.json includes only src/**/*, so
    pnpm --filter @objectstack/objectql typecheck never reaches scripts/. It is
    green with the error present — it was green before this repair and it is green
    after, and it proves nothing either way.
  2. The TEST_DEBT re-measure compiles a different project. remeasureProject
    (scripts/check-type-check-coverage.mjs) copies the package tsconfig and drops
    every test glob from exclude, so src/dry-run-hash-compat.test.ts becomes a
    root file — and its line 5 is
    import { runDryRun, type LegacyMetadataRow } from '../scripts/dry-run-hash-compat';.
    tsc pulls every transitively imported file into the program regardless of
    include, so the script is type-checked there: reached through the test, not
    through include.

The fix seeds the same predicate from __filename instead of
fileURLToPath(import.meta.url), in that one file.
Nothing about the predicate
changes: node's CJS loader hands __filename an absolute path that is already
symlink-resolved and percent-decoded, so both legs the guard rests on — realpath for
symlinks, and directory resolution when node is pointed at a directory — are
untouched. invokedAs(entryArg, selfPath), the shared core in
scripts/invoked-as.mjs, takes a path; isEntrypoint(import.meta.url) is only
the ESM way to seed it, and __filename is the CommonJS way. The comment at the
guard now says the divergence is forced by the package's module format, so the
next reader does not "restore consistency" and re-break the ledger.

Two remedies rejected: raising TEST_DEBT to 356 (the ledger is a shrink-only
ratchet and its own failure text forbids it), and adding "type": "module" to
packages/objectql/package.json (a package-wide module-format change whose blast
radius is far outside this card — filed as a finding instead). The guard itself is
untouched everywhere; weakening it would give back the defect this card closes.

The ledger measurement, before and after

The re-measure project rebuilt exactly as remeasureProject generates it for this
package — include: ["src/**/*"], exclude with the test globs dropped,
reconstructed typeRoots — with objectql's dependency closure built first
(pnpm --filter '@objectstack/objectql^...' build, CLOSURE_BUILD_EXIT=0):

tsc --noEmit --pretty false -p TMPDIR/tsconfig.debt-remeasure.json
BEFORE ca022387f2 356 error lines == the 356 CI measured, reproduced exactly
packages/objectql/scripts/dry-run-hash-compat.ts(279,40): error TS1470: The
'import.meta' meta-property is not allowed in files which will build into
CommonJS output.
AFTER 4d77499e10 355 error lines == the frozen TEST_DEBT number
no TS1470 on that file

diff of the two runs, sorted — exactly one line left, and nothing else moved:

88d87
< packages/objectql/scripts/dry-run-hash-compat.ts(279,40): error TS1470: ...

(The surviving TS1470 in the AFTER run is src/core-boundary.ratchet.test.ts:57,
pre-existing and part of the frozen 355.)

The guard still fires, and still stays inert on import

Runtime, via the documented pnpm tsx invocation:

direct run, snapshot argument exit=0, full report
through a RENAMED symlink, inside a
parent directory named "sym#dir" exit=0, full report (byte-identical)
no argument exit=2, "Usage: pnpm tsx ..."
imported by another module exit=0, CLI body did not run

Ablation, to show that symlink leg discriminates: with the pre-#10269 predicate
(process.argv[1] matched against a filename regex) put back on disk — mutation
confirmed by anchored grep, if (isProcessEntry()) count 0 and the injected regex
count 1 — the renamed-symlink invocation returns exit=0 and 0 bytes of output,
the silent-inert defect, while the direct run still prints the report. Restored from
the commit and re-verified by grep before measuring anything else.

Gates re-run on the new head

Union re-derived with node scripts/pm/dispatch-gates.mjs (no paths named) after
the final commit, and run at 4d77499e10. Exit codes captured before any pipe;
each line quotes the gate's own verdict:

error-code-casing exit=0 no lowercase error codes in 4354 scanned file(s)
slot-lookup exit=0 ratchet holds: 107 unswept site(s), none new
test-source-alias exit=0 72 packages with tests scanned
type-source-resolution exit=0 76 packages with a tsconfig.json scanned
engine-split-ratio exit=0 ADR-0076 D7 ratio 97.5%
affected-docs exit=0 308 self-test cases pass
entry-guard exit=0 125 scripts/ file(s), 47 self-test cases pass
nul-bytes exit=0 6231 text file(s), no raw ASCII control bytes
@objectstack/objectql test exit=0 225 files, 3983 tests passed
@objectstack/objectql typecheck exit=0

packages/objectql/scripts/** stays worth a gate of its own — #10756 is still open,
with a correction posted on it: that tree is invisible to the package's own
tsc -p, but it is not outside every tsc program, and this failure is what
measured the difference.


Generated by Claude Code


Generated by Claude Code

The three `import.meta.url === `file://${process.argv[1]}`` sites and the one
regex-basename site that #10086's `check:entry-guard` deliberately does not
scan. Both spellings are on that card's measured-broken list; both are replaced
by the two-leg predicate (`resolve` fast path + `realpath` for symlinked
checkouts) that `packages/cli/src/utils/invocation.ts` and
`scripts/invoked-as.mjs` already carry.
Also corrects `invocation.ts`'s header count: the #10086 census was ELEVEN
spellings across 33 files, not the "~8" estimate from that card's body.
Part of #10269
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DdCnBGcHeufjrq7drTD3wt
…in the tree
The CLI_NAME parity assertion is real, but it lives in `invocation.test.ts`, not
in the `invocation.cli-name-parity.test.ts` the header named — a reader grepping
for that filename finds nothing and could read the parity as unguarded.
Bounded in-place fix: same file and same class as this card's "~8 spellings"
header correction (a stale factual claim in the same module docstring), and the
correct form is pinned by the existing case at invocation.test.ts:210.
Part of #10269
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DdCnBGcHeufjrq7drTD3wt
@github-actions

github-actionsBot commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/cli, @objectstack/core, touching 5 documentable anchor(s).

19 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json 1793fb66fd1dacf2970808761be59cb2563206ab.

1 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/cli/src/utils/invocation.ts) — pages documenting those are invisible to this run
  • the SDK route bridge reached 45 of 221 client-bound route-ledger rows — the other 176 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run: node scripts/docs-audit/affected-docs.mjs --bridge-coverage

Coarse fallback — 39 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 1793fb66fd1dacf2970808761be59cb2563206abpackageMentionDocs.

Which tree this was computed on

This run read content/docs from 2627565e000772301bafbb79d7e30b2555178a8a — the merge of head 4d77499e1084a43bc254f528fe1def7bf3139f29 into base 1793fb66fd1dacf2970808761be59cb2563206ab, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 2627565e000772301bafbb79d7e30b2555178a8a && git checkout 2627565e000772301bafbb79d7e30b2555178a8a
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 1793fb66fd1dacf2970808761be59cb2563206ab 4d77499e1084a43bc254f528fe1def7bf3139f29 && git checkout -B drift-repro 1793fb66fd1dacf2970808761be59cb2563206ab && git merge --no-ff 4d77499e1084a43bc254f528fe1def7bf3139f29
node scripts/docs-audit/affected-docs.mjs --json 1793fb66fd1dacf2970808761be59cb2563206ab

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 1793fb66fd1dacf2970808761be59cb2563206ab → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@claude

claudeBot commented Aug 21, 2026

Copy link
Copy Markdown
ContributorAuthor

PM review + ruling — verified against the manifests and the diff, not the report. Approving.

Hazard 1: you turned "probably not allowed" into a proof, and I checked every leg

My brief said to measure whether importing a shared predicate is legitimate. Your answer is that no legal home exists, and all four supports hold:

  • (a) It would be a dependency cycle.packages/cli/package.json declares @objectstack/core: workspace:^and@objectstack/objectql: workspace:^ — so importing cli's isProcessEntry from either is a cycle. And it is unreachable anyway: cli's exports map is absent and it packs files: ['dist', …], so src/utils/invocation.ts has no subpath.
  • (b) The intersection is one package, and it is the wrong one. objectql ∩ core ∩ embed-objectql = @objectstack/spec, whose runtime deps are zod + pg-connection-string with no node: import in any non-test source (ADR-0076 D5). A node:fs/node:url predicate cannot live there.
  • (c)examples/embed-objectql does not depend on cli, and adding it would pull a 40-dependency CLI into an example whose stated purpose is "no kernel, no plugins".
  • (d) ⭐ And the precedent that looks like it transfers does not.packages/spec/scripts/build-docs.ts reaches root scripts/, but dry-run-hash-compat.tsis imported by packages/objectql/src/dry-run-hash-compat.test.ts — so that edge would put a root-scripts/ module into a package test's input graph, invisible to turbo. That is precisely the escape class check:cross-package-test-inputs exists for, and its self-test pins that shape.

So: four inline spellings, byte-identical, each carrying the divergence rule. My brief allowed for exactly this outcome and you reached it with evidence rather than by exhaustion.

⭐ The defect reproduced on the real files, including the case with no symlink

The strongest line in the report:

old guard under a real directory named a#bexit=0, 0 BYTES — inert, no symlink involved

with the ordinary-path control at exit=0/750 bytes and the repaired guard at 750 bytes under both a#b and a symlink. And on the compatibility checker, through a renaming symlink against a snapshot the direct run calls INCOMPATIBLE: before, exit=0 with zero bytes of output — a caller holding result.status reads a PASS; after, exit=1 with 1044 bytes byte-identical to the direct run.

Plus fire-on-import on the real file: an unrelated entry script whose path ends in dry-run-hash-compat.ts had the tool's CLI run inside its own process and die at exit=2 before its own code ran; afterwards, "IMPORTER ALIVE".

That is the difference between "this spelling is known-broken" and "here is the silent pass it produces."

⭐ The unexpected pass you read correctly

"my first attempt to probe encoding against dry-run-hash-compat.ts came back exit=1/1044 bytes on an a#b path, which confirms the card's two-family split rather than contradicting it"

The regex-basename form builds no URL, so it is immune to percent-encoding; the template form is immune to fire-on-import. A probe that passes when you expected a failure is the moment most likely to be written off as "repro didn't work" — you diagnosed it as the two families behaving as documented and re-ran the encoding leg against the right one.

Ruling on the open question: A, and file D if a fifth site appears

Your framing decides it, and the argument against B is the one that matters:

"a gate that accepts 'an inline copy matching this template' teaches the next author that copying is blessed, which is the drift #10086 spent 33 files removing"

B would make the gate assert a SPELLING rather than a ROUTE — strictly weaker than the invariant #10086 landed, and actively counterproductive for machine-written code. C is ruled out arithmetically: the set it excludes is precisely these four sites. D is the only option that closes the class, and it is the right escape hatch, but a new workspace package to host a 16-line predicate is a published-surface decision that four non-shipping files do not justify today.

Two facts you surfaced make A honest rather than lazy: the gate's walk collects only .mjs/.js/.cjs, so three of these four .ts files would remain invisible even after a scope change; and the gate's rule — process.argv[1] anywhere but scripts/invoked-as.mjs is a failure — is one every correct inline spelling here violates by construction. Widening without D would therefore red the very fix it was widened to protect.

Recorded on the card. If a fifth site appears, D is the move.

The rest

#10756 came out of your own ablation, which is the best provenance a finding can have: you injected const x: number = "string" into packages/objectql/scripts/, confirmed it landed by grepping the injected text, and typecheck still exited 0 with the file appearing 0 times in tsc --listFiles — while objectql sits in neither the DEBT nor the EXEMPT ledger. Correctly labelled.

Arming.


Generated by Claude Code

…compiles as CommonJS
`packages/objectql/package.json` declares no `"type"`, so under the repo-wide
`module: NodeNext` every file in the package has CommonJS format, and the
`import.meta.url` seed the guard arrived with is a hard TS1470 compile error
there. It surfaced as a ratchet failure rather than a build failure because the
package's own `tsconfig.json` includes only `src/**/*`: the script is reached
through `src/dry-run-hash-compat.test.ts`, which imports it, and only the
TEST_DEBT re-measure (which drops the test globs from `exclude`) compiles that
test. `@objectstack/objectql` measured 356 against a frozen 355.
`__filename` is the CommonJS seed for the same predicate: node's CJS loader
hands it an absolute path that is already symlink-resolved and percent-decoded,
so both legs the guard rests on — realpath for symlinks, directory resolution
for `node <dir>` — are unchanged. The comment now states that the divergence
from `packages/cli/src/utils/invocation.ts` is forced by the module format, so
the next reader does not "restore consistency" and re-break the ledger.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DdCnBGcHeufjrq7drTD3wt
@os-zhuang
os-zhuang added this pull request to the merge queueAug 21, 2026
Merged via the queue into main with commit 6130cb7Aug 21, 2026
35 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-10269-symlink-blind-entry-guards branch August 21, 2026 11:43
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/mskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants

@os-zhuang@claude