.cts, .mts and .ts joined _EXECUTABLE_SUFFIXES in #548, so Path.suffix now classifies TypeScript declaration files as executable content. A skill whose only payload is types.d.cts scans as Executable=Yes.
The obvious fix is to exclude .d.cts/.d.mts by filename. I implemented exactly that in #480, measured it, and withdrew it as unsafe.
Node executes these files. On v24.18.0 node evil.d.cts runs, and node t.d.ts runs under type stripping. The .d. infix is a TypeScript convention, not a loader rule. So with a name-based exclusion, evil.d.cts containing execSync(...), no shebang and no exec bit, flips from flagged to unflagged. Renaming payload.cts to payload.d.cts would evade the scan.
That matters most at build_context.py:715 and :1319, which call is_executable_content(path, b"") with no mode. Suffix is the only live signal there, so neither the shebang nor the exec-bit arm can compensate.
Filing rather than patching, because the false positive is real but the cheap fix trades it for a false negative. Separating a declaration file from a runtime module needs content inspection, not the filename.
.cts,.mtsand.tsjoined_EXECUTABLE_SUFFIXESin #548, soPath.suffixnow classifies TypeScript declaration files as executable content. A skill whose only payload istypes.d.ctsscans as Executable=Yes.The obvious fix is to exclude
.d.cts/.d.mtsby filename. I implemented exactly that in #480, measured it, and withdrew it as unsafe.Node executes these files. On v24.18.0
node evil.d.ctsruns, andnode t.d.tsruns under type stripping. The.d.infix is a TypeScript convention, not a loader rule. So with a name-based exclusion,evil.d.ctscontainingexecSync(...), no shebang and no exec bit, flips from flagged to unflagged. Renamingpayload.ctstopayload.d.ctswould evade the scan.That matters most at
build_context.py:715and:1319, which callis_executable_content(path, b"")with no mode. Suffix is the only live signal there, so neither the shebang nor the exec-bit arm can compensate.Filing rather than patching, because the false positive is real but the cheap fix trades it for a false negative. Separating a declaration file from a runtime module needs content inspection, not the filename.