Repository navigation
Releases
Releases · YawLabs/ctxlint
v0.28.2
Changed
- The
ctxlintlauncher's oam floor is now 0.19.5, up from 0.19.1.OAM_MINtracks the latest oam release, and ctxlint was re-verified on oam 0.19.5 through the launcher (npm run verify:oam-floor: initialize + tools/list). An oam host at 0.19.4 or earlier now hands off or falls back to Node instead of running the CLI in-process.
Security
- Dev-dependency updates for the open Dependabot and
pnpm auditadvisories: vite 8.3.8, vitest and@vitest/coverage-v84.1.12, zod 4.6.7, and overrides for hono,@hono/node-server(now 2.x), fast-uri, undici, body-parser, qs, ip-address and minimatch, plus glob 14.0.5 (was 13.0.6). Of these, glob, zod, fast-uri and minimatch are bundled intodist/index.jsand so reach the published package; the rest are build and test tooling only.
v0.28.1
Changed
- The
ctxlintlauncher's oam floor is now 0.19.1, up from 0.18.0.OAM_MINtracks the latest oam release, and ctxlint 0.28.0 was re-verified on oam 0.19.1 through the launcher (npm run verify:oam-floor: initialize + tools/list, plus atools/call). An oam host at 0.19.0, a 0.19.0 dev build or 0.18.x now hands off or falls back to Node instead of running the CLI in-process.
Fixed
server.jsonnow launches the MCP server. Its npm package carries the positionalserveargument, so a registry client that runsnpx @yawlabs/ctxlintfrom it starts the MCP server instead of a one-shot lint of its working directory.
Internal
scripts/check-oam-floor.mjsnow runs when invoked through a junctioned or symlinked checkout. Its direct-run guard comparedprocess.argv[1]with the main module's URL, which Node realpaths andargv[1]is not, so from such a path the check skippedmain()and exited 0 with no output. Both sides are now realpath'd (ported from yaw-mcp 1.1.2'sverify-oam-floorguard).
v0.28.0
Changed
- The MCP server now sends
instructions, and the four audit tools returnsummaryfirst. The initialize result carries a short plain-ASCII routing note (well under 2000 bytes): which audit tool covers which file class, thatctxlint_fixwrites unlessdryRunis true, and thatctxlint_token_reportis the cheap probe. Yaw MCP renders a server's instructions once per namespace per session.ctxlint_audit,ctxlint_mcp_audit,ctxlint_session_auditandctxlint_skill_auditserialize their result as{summary, version, scannedAt, projectRoot, files, ...}without the two-space indent: Yaw MCP caps a tool result at 100000 bytes by default and cuts from the tail, so on a large project the summary counters used to be the first thing lost. The fields and their values are unchanged; a client that parses the JSON sees no difference, and one that read the raw text sees one compact line. - The
ctxlintlauncher's "no usable oam" error now names the fix for what it found. UnderCTXLINT_RUNTIME=oamit used to print "Install or update from https://oamjs.org" for every cause. It now saysoam self-updatewhen an outdated oam was found, to check the binary when one would not run, to pointOAM_BINsomewhere real when it names a missing file, and to install oam only when none was found -- and on Linux other than x64, where oam publishes no build, that there is nothing to install.CTXLINT_RUNTIME=nodeis offered last in every case. - The launcher finds an oam installed to
OAM_INSTALL_DIR. oam's own install-target variable is now searched first, before%LOCALAPPDATA%\oam\bin,~/.oam/binand PATH; an oam installed there and not on PATH was never found. - A handoff from an oam host no longer passes oam's permission flags on to Node. oam 0.18.0 appends its
--permission/--allow-*execArgv entries to every child's NODE_OPTIONS. When the launcher runs on oam (an oam below the floor, orCTXLINT_RUNTIME=node), it now spawns its child with those tokens removed from the inherited NODE_OPTIONS; Node 22 refuses to start at all with an oam-only flag such as--allow-netthere (exit 9). It cannot stop the host oam re-adding its own flags, which happens only when the launcher itself was started under--permission. - The handoff from an oam host inherits stdio unless that oam is older than 0.9.0. Piping was a workaround for pre-0.9.0 oam treating
stdio: 'inherit'as'pipe'; from 0.9.0 on the launcher hands over the same fds instead of copying every MCP/LSP byte through itself. An oam host whose version cannot be read still pipes.
Internal
release.shstep 3 now also runsscripts/check-oam-floor.mjs(ported from @yawlabs/aws-mcp:OAM_MINagrees with every floor claim in the launcher and its test, and is not behind the latest oam release --CTXLINT_ALLOW_STALE_OAM=1releases on an old floor deliberately),scripts/verify-oam-floor.mjs(hostsbin/ctxlint.mjs servethroughoam runon the release machine's oam and completes initialize + tools/list; a machine with no oam is a warning, an oam below the floor fails), and@yawlabs/mcp-compliance^0.20.4, now a devDependency on the line Yaw MCP grades with, at--strict --min-grade A, rewritingcompliance-badge.svgfrom the run. An unreachable GitHub API, a missing oam, a missing mcp-compliance install and skipped compliance tests each print a warning rather than passing silently. The offline half of the floor check also runs in the test suite (src/__tests__/oam-floor.test.ts). New scripts:check:oam-floor,verify:oam-floor,compliance.- The compliance badge is regraded with mcp-compliance 0.20.4 against the current seven tools: grade A (100%), 45/45 passed, 5 capability-gated tests skipped. The previous badge dated from 2026-04-16, before
ctxlint_skill_auditexisted.
v0.27.6
Fixed
- HTML/XML comment stripping no longer mis-handles a malformed nested comment such as
<!<!-- x -->-- y. Context-file line classification blanks complete<!-- ... -->pairs with length-preserving filler instead of deleting them before testing for an unclosed opener, so deletion can no longer splice<!and--into a<!--that was never written and latch the rest of the file as comment. The Maven POM reader (maven-modules) strips comments to a fixed point, so a comment re-formed by one removal pass is not read as live<module>entries, and treats a POM with an unclosed<!--as unreadable instead of reading the elements after it.stripInlineHtmlCommentsalso loops to a fixed point; its output is unchanged, since its filler cannot form a new pair. Resolves CodeQLjs/incomplete-multi-character-sanitizationalerts #4, #5 and #6 (ctxlint renders no HTML, so these were correctness edge cases, not injection).
v0.27.5
Internal
- The Windows 8.3 short-path probe in the
findRenamestests now reads the temp-dir path with cmd delayed expansion (cmd /d /v:on,!VAR!) instead of%VAR%. cmd substitutes%VAR%before it parses the line, so moving the path into an environment variable (#89) kept the command text constant but still let a&or)in the path act as a command operator; a!VAR!value is substituted after parsing and stays data (CodeQLjs/shell-command-injection-from-environment, alert #3)./dalso skips any registry AutoRun command. Test-only; the CLI and server are unchanged.
v0.27.4
Security
simple-gitmoves from 3.36.0 to 4.0.2, andpnpm auditgoes from 10 findings (3 critical, 4 high, 3 moderate) to 0.simple-gitis bundled intodist/index.js, so this reaches every install. The bump closes GHSA-x6jw-m9v5-85vh (critical), GHSA-g4wm-2vf7-vfgr and GHSA-858h-whjf-mvg5 (high) insimple-gitand GHSA-v5rq-49vh-5v5c (critical) in its@simple-git/argv-parser; no patched 3.x exists, since 3.36.0 is the last 3.x release and GHSA-x6jw is fixed only in 4.0.1. The bundled MCP SDK moves from 1.30.0 to 1.32.1 (GHSA-6qxp-vccf-f47h, high), and its bundledfast-urifrom 3.1.7 to 3.1.8 (GHSA-hrr3-gc8f-f4qj). Development-scope only:proxy-addr2.0.8 (GHSA-jqcg-44mw-7w3h),ip-address10.7.3 (GHSA-j6r3-76f7-8jcv, GHSA-h3mg-xc3c-68pw) andsource-map-js1.2.2 (GHSA-68fv-2mgg-jv7q). Thepnpm.overridesfloors forip-addressandfast-urimove to the patched versions.
Changed
- git now runs without inherited
GIT_*environment variables.simple-git4 strips every inheritedGIT_*variable (andEDITOR,VISUAL,PAGER,SSH_ASKPASS) before it spawns git, and ctxlint accepts that rather than allow-listing any back. A repository reached only throughGIT_DIR/GIT_WORK_TREE, or one that needsGIT_CONFIG_COUNT/KEY/VALUE(a common way to setsafe.directoryin containers), is no longer seen as a git repository: the staleness and rename checks skip, as they do outside any repository. Run ctxlint from inside the work tree, or setsafe.directoryin a git config file, instead. In return, a parent repository'sGIT_DIRno longer leaks into ctxlint's lookups when it runs from a git hook. - The oam floor moves from 0.15.2 to 0.18.0, so the
ctxlintlauncher no longer runs the CLI on an older oam. ctxlint is verified on one oam release at a time, and the floor keeps the launcher off anything older than that release. 0.18.0 is verified on the published aarch64-pc-windows-msvc binary, checksum matched against the release SHA256SUMS, through the launcher withOAM_BINpointing at it andCTXLINT_RUNTIME=oam:ctxlint servecompletes a full MCP handshake listing all 7 tools (the same 7 as on Node),ctxlint_auditon this repo answers identically to Node apart from itsscannedAttimestamp (its git-backed checks rungitthrough simple-git 4.0.2 on oam; the rename check answers identically to Node), and the server process is that binary (oam-aarch64-pc-windows-msvc.exe run .../dist/index.js -- serveas the launcher's child). A host that launchesoam run bin/ctxlint.mjson 0.18.0 runs the CLI in-process, as before. If the launcher finds only an oam from 0.15.2 to 0.17.x, it now falls back to Node under the defaultCTXLINT_RUNTIME=auto, and exits with an error underCTXLINT_RUNTIME=oam-- it says so on stderr, naming the version it found and the floor (measured with oam 0.17.0:is oam 0.17.0, older than 0.18.0; using Node instead.); an oam 0.17.x host running the launcher hands the CLI off to Node the same way. On Node every check and every MCP tool answers the same, so nothing is lost but the runtime. A client that runsoam run /path/to/ctxlint/dist/index.jsdirectly bypasses the launcher and keeps the oam it names. Runoam self-update, or setCTXLINT_RUNTIME=nodeto make the choice explicit.
Internal
release.shwaits up to 600 s, not 120, for npm to serve a new version before the MCP Registry step, and polls npm's uncached per-version document up to 120 times 5 s apart in its final check instead of reading the CDN-cachednpm viewonce after 3 s. On 2026-09-29 the @yawlabs/fetch-mcp 0.8.2 release spent 295 s of its 300 s gate waiting for npm to serve the new version. Release tooling only; the server itself is unchanged.release.shruns every mcp-publisher call to the MCP Registry, each login and each publish attempt, under coreutilstimeout(MCP_PUBLISH_TIMEOUT_S, default 90 s) where one (or Homebrew'sgtimeout) is on PATH -- without one the call runs unbounded, and a warning says so -- because mcp-publisher waits for the registry's answer with no limit of its own and a registry that never answered would have hung the release. A publish attempt that gets no answer is retried on the same 30, 60 and 90 s clock as the registry's own 429-504: one the limit stopped, which used to hang the step, and one whose connection failed or dropped (the client'serror sending requestorerror reading response), which used to fail it at once. A login the limit stopped says the registry did not answer; one that failed any other way says how to read mcp-publisher's output (a 401 is the registry refusing the token exchange; a 429, a 5xx or a connection error is the registry or the network) instead of naming token scopes, which never fail a login: the registry reads the org role at login and refuses only at publish. A publish refused with a 403 says what the io.github.YawLabs namespace takes: a YawLabs org Owner whose token can read org roles. Release tooling only; the server itself is unchanged.
v0.27.3
Internal
release.shreads npm's per-version document wherever it asks whether this version is published, and a resume no longer dies on npm's E403. Step 6's "already published -- skipping" check usednpm view, which reads the whole packument that registry.npmjs.org serves from Cloudflare's edge for up to 300s, so a re-run right after a later step failed could publish again and stop on npm's "cannot publish over the previously published versions" E403 as if it were a token problem. That check now goes throughnpm_version_live(the uncached per-version document the MCP Registry itself fetches, now also cache-busted and capped at 20s), and the workstation publish loop treats that E403 as the skip it should have been, with a warning in place of the success line. Step 8's MCP Registry retry now also waits out the live registry's (v1.8.1) wording for trouble on npm's side -- "Likely transient, retry later" (429, 5xx, an inconclusive 404) and "failed to fetch package metadata from NPM" -- and its not-found shape needsnot found (status: N)with the version in the same output, so a package missing outright still fails on the first attempt. It also waits out the registry's own HTTP 429, 502, 503 and 504, and logs in again before the first attempt and every retry, since its login comes before the npm gate and registry tokens last 5 minutes.
Documentation
- The README's Add to Yaw MCP button now sits directly under the title, and the follow badge links to @YawLabs. The one-click install button and its one-line caption moved up from further down the page, so they are the first thing under the name on npm and GitHub, and the X badge at the bottom points at @YawLabs instead of @TokenLimitNews, and the Token Limit News link is gone. npm shows the README from the published package, which is why it takes a release to carry this there; the package's code is unchanged from 0.27.2.
v0.27.2
Security
- The MCP SDK devDependency moves from
^1.29.0to^1.30.0, andpnpm auditgoes from 28 findings (10 high, 17 moderate, 1 low) to 0. Every finding was development-scope, and all but the directvitestdevDependency were transitive, through the SDK or the build/test toolchain:fast-uri3.1.2 -> 3.1.7,hono4.12.25 -> 4.13.7,@hono/node-server1.19.14 -> 1.19.17,ip-address10.2.0 -> 10.7.0,qs6.15.2 -> 6.16.0,postcss8.5.19 -> 8.5.28,nanoid3.3.16 -> 3.3.19, the lockfile'sbrace-expansion5.0.7 -> 5.0.12, andvitest/@vitest/coverage-v8/@vitest/mocker4.1.9 -> 4.1.11 (Dependabot auto-dismissed seven of its alerts --honoGHSA-8j4g and GHSA-g6gw,qsx2,nanoidandbrace-expansionx2 -- so those showed up only inpnpm audit). Thepnpm.overridesfloors are raised to each advisory's first patched version (hono^4.13.5,@hono/node-server^1.19.15,fast-uri^3.1.6,ip-address^10.3.1,qs^6.16.0,postcss^8.5.23,brace-expansion^5.0.9) so a lockfile refresh cannot slide back into the advised ranges; with pnpm, raising the SDK floor alone re-resolves nothing transitive. Of the alerted packages, onlyfast-uri(via the SDK'sajvvalidator) was inlined into the published 0.27.1dist/index.js: that bundle carried 3.1.2, inside the advised range of six high-severity host-confusion and SSRF advisories, and the bundle now carries 3.1.7. ajv resolves only ctxlint's own tool schemas, so the vulnerable path was never reachable from user input, but the shipped copy is updated regardless. The lockfile'sbrace-expansionis inlined as well now, through theglob/rawchange in the next entry; none of the others is in the bundle. dist/index.jsnow bundles the lockfile's patchedbrace-expansioninstead of the pre-5.0.6 copy vendored insideglob.glob's default entry point (since 13.0.2) is a self-containedindex.min.jswith its ownminimatchandbrace-expansioninlined, and the 0.27.1 bundle shipped that copy: its numeric-range loop has nomaxcap, so it is open to GHSA-jxxr-4gwj-5jf2 and the laterbrace-expansionDoS advisories. Nopnpm.overridesfloor can reach code vendored that way, so the 0.15.1 entry below was not accurate when it said the>=5.0.6override fixed "the bundled copy": the override only moved the lockfile's copy, which the bundle never used.src/core/scanner.tsandsrc/core/checks/paths.tsnow import fromglob/raw,glob's unbundled entry that depends onminimatchas a normal package, so esbuild inlines the lockfile'sminimatch10.2.5 andbrace-expansion5.0.12, with themax,maxLength,maxDepthandmaxRewritescaps. Sameglobversion and API;dist/index.jsgrows by about 107 KB against published 0.27.1, about 94 KB of it from this change, because the unminified modules replaceglob's minified one. Neither Dependabot norpnpm auditsees vendored code, so this was found by reading the published bundle.fixtures/masked-exit-status/package.jsonmoves itsvitestrange from^2.0.0to^4.1.11, closing the one Dependabot alert (GHSA-5xrq-8626-4rwp) raised against a manifest that is never installed: the fixture is linter input for thecommands/exit-status-maskedtest, has no lockfile, is not a workspace, and is excluded from the npm tarball. The test reads only dependency key names, so the value is inert.- Pre-commit users pin the version through
.pre-commit-hooks.yaml(npx @yawlabs/ctxlint@<version>) and need to bumprevto pick up the patched bundle. GitHub Action users on the defaultversion: latest,npxusers and MCP Registry clients get it on their next resolve.
v0.27.1
Changed
- npm and MCP Registry listing metadata: bugs URL, core keywords, and server.json title/repository/websiteUrl
release.shwrites a## [x.y.z]entry for every release, and the GitHub release notes come from it. Step 4 promotes[Unreleased]when it has content, leaving a fresh, empty[Unreleased]heading above the new section for the next change, and otherwise generates the section from the commit subjects since the previous tag; Keep-a-Changelog link references, when the file has them, are moved along with it. Step 7 then takes the release notes from that entry instead of fromgit logsubjects, and its backstop fails on any missing entry rather than only when[Unreleased]had content. Before this, a release with nothing under[Unreleased]got no entry at all -- the[0.26.0]section below was backfilled by hand in #75 -- and its release page listed raw commit subjects, its own version-bump commit included, which is how v0.26.0's was published. Promoting also consumed the[Unreleased]heading, so the next change had to add it back (#76).
Internal
release.shgives npm propagation twice the time before the MCP Registry publish. Step 8 waits for npm to serve the new version before registering it, then retries the registry publish when it answers that the version is not on npm yet. The v0.27.0 release exhausted the 60s wait and succeeded only on the last of three retries, about 150s after the publish, while v0.25.4 the same day cleared the wait in under 60s. The wait is now 120s and there are four retries (30s, 60s, 90s apart), 300s in all; the extra time elapses only on a run that is still propagating. Both loops were exercised against a stubbedmcp-publisher: a publish that fails three times still succeeds on the fourth attempt, and one that fails four times exits through the existing failure message that names what has already landed.
v0.27.0
Fixed
- Glob-scoped
.ctxlintignorerules show up in the "Ignore rules" report when they drop nothing. A rule with a file glob (tokens sub/*.md) counted as fired as soon as its glob matched any file, whether or not it dropped a finding, so a rule that suppressed nothing was never listed as drift -- unlike every other ignore rule, which counts as fired only when it drops something. And when the only rules loaded were glob-scoped and none dropped anything, the report was skipped entirely, hiding a rule whose glob matched no file and any rule missing a reason. A glob-scoped rule now counts as fired only when it drops a finding, and the report is built whenever any rule is loaded. Glob-scoped rules are applied one at a time in file order, so the first matching rule still wins and is the one credited. As a result, JSON_meta.ignoreReportand the text footer now also appear for a run whose only rules are glob-scoped.
Internal
- README:
.ctxlintignorewas named in the Options block and theexcludeprose, but its format was documented nowhere. A new "Ignore file" section covers thecheckName [fileGlob] [# reason]line format, what the glob is matched against, the space required before#, rule order relative toignoreRules, the shared report,--no-ignore-fileand--watch.ignore-file.test.ts(new) covers the parser and loads the README example expecting real check names and a reason on every line;audit.test.tsgains the first end-to-end.ctxlintignoretests, including both reporting fixes above.