feat: renovate updates and security improvments - #2
Merged
Conversation
Git stores a symlink as a file whose content is its target, so one reaches the review looking like ordinary one-line text. Nothing here interpreted a file mode, so both write sites wrote it back as text — and both resolve the destination, so the write landed wherever the link pointed, outside the workspace. The path check could not see this: it settles the path text, and a symlink escapes without a suspicious path anywhere. The reachable case is an unmarked symlink in a jj review. An unmarked file is never read from the working copy, so no staleness check runs on it, yet it still becomes a restore instruction for the helper script. Refuse at checkReviewedFile, the one place both backends pass through and which runs for every file, marked or not. Regular modes are an allowlist: an unrecognised mode is one nobody has reasoned about. The write sites are hardened too, for when the working copy and the patch disagree: O_NOFOLLOW when discarding, and an unlink before both the redirect and the cp in the jj helper. The discard environment moves out of index.ts so the integration test drives the real one rather than a copy that could not have caught this.
The CI job hands GITHUB_TOKEN to a step as GH_TOKEN, so three supply-chain gaps compounded: no permissions block (the token inherited whatever the repository default happened to be), both actions on floating tags that can be retagged onto attacker code, and a jj tarball installed as root with neither a pinned version nor a digest check. Declare permissions: contents: read, pin actions/checkout and setup-bun to commit SHAs, and pin jj to v0.44.0 verified against a recorded SHA-256. jj publishes no checksum file, so the digest was recorded by hand: it catches a later swap of the asset, it does not attest the build.
muzomer
force-pushed
the
feat-renovate-and-security-improvements
branch
from
August 30, 2026 19:31
7a59a62 to
88666a5
Compare
Renovate cannot manage the 'latest' dist-tag, and a range as loose as '^5' never produces a PR because every new 5.x already satisfies it — both bumps would land silently in bun.lock instead of arriving as a reviewable diff. 'latest' was already decorative: CI runs 'bun install --frozen-lockfile', so the lockfile pin won and the dist-tag only took effect when someone regenerated the lockfile, moving the version with no diff anyone reviewed. Both ranges are set to the versions bun.lock already resolved, so no install changes. hunkdiff is left at ^0.20.0 — a caret on a 0.x version is already tight (>=0.20.0 <0.21.0). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V5yBUZqwwtDFrkKLtANyLw
Configures the hosted Mend Renovate app to manage npm devDependencies and GitHub Action pins. ci.yml already SHA-pins actions by hand, but a hand-pinned SHA is frozen forever — we get the supply-chain benefit and none of the security updates. config:best-practices keeps the pin and moves it. Cooldown against compromised uploads uses two settings that must go together: minimumReleaseAge alone only marks an internal check as pending, so Renovate still opens the PR and automerge can merge it unreviewed once the check flips green. internalChecksFilter 'strict' suppresses the PR until the version is old enough, turning the cooldown into an actual filter rather than a delay. Because that cooldown would also delay a release that fixes a CVE, security updates bypass both the age rule and the weekly schedule. hunkdiff never automerges: it is imported only as 'import type', so it is erased at compile time and 'bun test' passes identically on any version. Only tsc covers it, for 4 interfaces. It is also 0.x, where Renovate calls a breaking change a 'minor'. Written as JSON5 so the reasoning lives next to the settings it explains. Config is inert until the app is installed and Dependabot alerts are enabled. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V5yBUZqwwtDFrkKLtANyLw
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Harden path handling, pin CI supply chain, add Renovate
Three related strands: the extension acts on paths and file modes parsed out of patch text, CI hands a token to an unpinned toolchain, and dependencies had no update mechanism.
Refuse unsafe paths (
src/patch/paths.ts,src/review/check.ts)Every path this extension reads, writes, or deletes comes from patch text, and every place it lands normalises
..rather than confining it —join("/repo", "../../etc/x")is/etc/x, not an error. Containment now happens before the join. Refusing costs nothing: git and jj both emit repo-root-relative paths, and git's one cwd-relative mode omits outside files rather than reaching them with...Refuse symlinks and submodules (
src/review/check.ts)Git stores a symlink as a file whose content is its target, so it reached review looking like ordinary one-line text. Nothing interpreted a file mode, so both write sites wrote it back as text and resolved the destination — landing the write wherever the link pointed, outside the workspace. The path check couldn't catch this: a symlink escapes without a suspicious path anywhere.
Refused at
checkReviewedFile, the single point both backends pass through, for every file whether marked or not. Regular modes are an allowlist — an unrecognised mode is one nobody has reasoned about. Write sites are hardened for when the working copy and the patch disagree:O_NOFOLLOWon discard, and an unlink before both the redirect and thecpin the jj helper.The reachable case was an unmarked symlink in a jj review: unmarked files are never read from the working copy, so no staleness check runs, yet the file still becomes a restore instruction.
Pin the CI supply chain (
.github/workflows/ci.yml)CI passes
GITHUB_TOKENto a step, and three gaps compounded: nopermissionsblock, both actions on floating tags that can be retagged onto attacker code, and a jj tarball installed as root with no pinned version or digest. Nowcontents: read, actions pinned to commit SHAs, and jj pinned to v0.44.0 against a recorded SHA-256. jj publishes no checksum file, so the digest was recorded by hand — it catches a later swap of the asset, it does not attest the build.https://claude.ai/code/session_01V5yBUZqwwtDFrkKLtANyLw
Two notes on using it: the branch isn't pushed yet, so gh pr create needs a jj git push first — tell me if you want both. And the last section is a checklist for you specifically; if you'd rather it not live
in the PR body permanently, it fits better as a comment.
✻ Cooked for 22s · done 6:14 PM
❯ I mean PR description for all the changes in the branch, but please keep it precise and sh
Right — the branch has earlier commits too. Let me look at the whole thing.
Ran 4 shell commands
Here's the description for the whole branch, kept short:
Harden path handling, pin CI supply chain, add Renovate
Three related strands: the extension acts on paths and file modes parsed out of patch text, CI hands a token to an unpinned toolchain, and dependencies had no update mechanism.
Refuse unsafe paths (
src/patch/paths.ts,src/review/check.ts)Every path this extension reads, writes, or deletes comes from patch text, and every place it lands normalises
..rather than confining it —join("/repo", "../../etc/x")is/etc/x, not an error.Containment now happens before the join. Refusing costs nothing: git and jj both emit repogit's one cwd-relative mode omits outside files rather than reaching them with
...Refuse symlinks and submodules (
src/review/check.ts)Git stores a symlink as a file whose content is its target, so it reached review looking like ordinary one-line text. Nothing interpreted a file mode, so both write sites wrote it back as text and resolved the destination — landing the write wherever the link pointed, outside the workspace. The path check couldn't catch this: a symlink escapes without a suspicious path anywhere.
Refused at
checkReviewedFile, the single point both backends pass through, for every file whether marked or not. Regular modes are an allowlist — an unrecognised mode is one nobody has reasoned about. Write sites are hardened for when the working copy and the patch disagree:O_NOFOLLOWon discard, and an unlink before both the redirect and thecpin the jj helper.The reachable case was an unmarked symlink in a jj review: unmarked files are never read from the working copy, so no staleness check runs, yet the file still becomes a restore instruction.
Pin the CI supply chain (
.github/workflows/ci.yml)CI passes
GITHUB_TOKENto a step, and three gaps compounded: nopermissionsblock, both actions on floating tags that can be retagged onto attacker code, and a jj tarball installed as root with no pinned version or digest. Nowcontents: read, actions pinned to commit SHAs, and jj pinned to v0.44.0 against a recorded SHA-256. jj publishes no checksum file, so the digest was recorded by hand — it catches a later swap of the asset, it does not attest the build.Add Renovate (
renovate.json5,package.json)Hosted Renovate app, scoped to npm devDependencies and GitHub Actions.
config:best-practipinGitHubActionDigests` — the SHAs pinned above are otherwise frozen forever, giving usthe supply-chain benefit and none of the security updates.
@types/bun: latestandtypescript: ^5are pinned first, because Renovate can act on neither:latestis a dist-tag, and every 5.x already satisfies^5. Both are set to the versionsbun.lockalready resolved, so nothing installs differently.A 7-day cooldown (
minimumReleaseAge+internalChecksFilter: strict— the first alone s automerge fire unreviewed) holds back fresh uploads. Security fixes bypass it, or thesetting would delay CVE patches and make us less safe.
hunkdiffnever automerges: it'serased at compile time andbun test` gives zero signal on a bump.Also
Beads issue tracking,
.perles/config.yaml, andCLAUDE.mdfor agent context.