Every new TypeScript project taxes you the same way before you write a single line that matters. tsconfig with the right target and module resolution. Jest configured for both local runs and CI, with coverage that doesn’t choke. A GitHub Actions workflow that actually runs your tests. A publish pipeline if it’s a package. VSCode debug configs so you’re not printf-debugging by hour two. None of it is hard. All of it is the same every time, and all of it is exactly boring enough that I’d rather not re-derive it from a half-remembered previous repo.
So I don’t. I have ts-scaffold, and every new TS project — package or app — starts from it. git clone --depth=1, rm -rf .git, done. Or click Use this template on GitHub. Either way you’re at line one in under a minute, not after answering a wizard.
Why not a generator
Every ecosystem has its yeoman-style tool: answer six prompts, get a folder. I don’t want that. Generators optimize for covering everyone’s preferences, which means they either ship a config with forty knobs or they make choices I’d have made differently and now I’m un-making them project after project. A personal scaffold has the opposite property: every choice in it is already mine. I made the tradeoff once, wrote it down as a file, and now I disagree with myself zero times per project instead of once per prompt.
The one trick worth stealing: never store the npm token
This is the part of the scaffold I’d recommend copying even if you throw away everything else.
The naive way to publish from CI is to drop an NPM_TOKEN into your repo secrets and let npm publish read it. That token is a long-lived credential with full publish rights, sitting at rest in a repo whose infra you don’t control. And that exact pattern is now the single most productive thing an attacker can steal. When the self-spreading npm worm known as Shai-Hulud hit, it didn’t need an exploit — it just went looking for tokens where everyone leaves them. From BleepingComputer’s writeup:
If publish tokens are found on the compromised system in environment variables or the ~/.npmrc configuration file, the malicious script identifies the packages that the victim can publish, adds the payload, and republishes them.
A stored NPM_TOKEN lives in exactly those two places every CI run. And it leaks in ways you don’t plan for: the GhostAction campaign exfiltrated 3,325 secrets from 817 repositories by injecting a malicious workflow disguised as a security check — npm tokens among the most-stolen. Once one leaks, it republishes malware under your name until you notice.
So publish.yml in the scaffold stores no npm token at all. At publish time it calls a scoped, read-only secrets broker — a single-key endpoint — using two innocuous repo secrets: NPM_TOKEN_URL and NPM_BROKER_SECRET. The broker hands back the real token only when asked, only for that one purpose.
# publish.yml — fetch the credential at use-time, don't bake it in
- name: Resolve npm token from broker
run: |
TOKEN=$(curl -sf "$NPM_TOKEN_URL" -H "authorization: $NPM_BROKER_SECRET")
echo "::add-mask::$TOKEN"
echo "//registry.npmjs.org/:_authToken=$TOKEN" > ~/.npmrc
env:
NPM_TOKEN_URL: ${{ secrets.NPM_TOKEN_URL }}
NPM_BROKER_SECRET: ${{ secrets.NPM_BROKER_SECRET }}
If a workflow log leaks or a misbehaving action echoes a secret, what leaks is a pointer — a credential that can do nothing on its own except ask a broker. The real publish token never rests in the repo. It’s the “fetch the credential at use-time, don’t bake it in” idea applied to the one place almost nobody bothers: their own publish pipeline.
The rest that earns its keep
CI on every branch, publish only on main. Jest runs on every push to any branch — signal before you open a PR. On main, the same workflow publishes. Push to main means shipped.
Version bump automation. A script increments the patch position automatically, so “did I forget to bump” stops being a thing I check. It’s the fourth position (x.x.x.1) — a detail I’ve argued with myself about, but it’s consistent and I stopped fighting it.
VSCode debug configs, pre-made. Node attach, launch-and-debug-current-file, and debug-current-Jest-test. The three situations I actually hit. I don’t reach for console.log in a scaffolded project because the debugger is one keystroke away.
Everything else is sane defaults — an opinionated ES6 tsconfig, a .NET-style Main entry point, build/watch scripts, Prettier. None of it clever. That’s the point: clever is what you don’t want in the part of your stack you never look at again.
Where I’d push back on myself
Be honest about the broker: today the better default is npm trusted publishing with OIDC, generally available since July 2025, which issues short-lived per-workflow credentials and stores nothing at all. The scaffold predates it. If you’re setting up fresh, reach for OIDC first — the broker earns its place only for the long-lived secrets OIDC doesn’t cover.
The rest is opinionated and you should expect friction if your taste differs. ES6 as a target won’t suit everyone. The Main-program pattern is a personal habit. If you don’t publish to npm, half the scaffold is dead weight you’ll delete on day one. That’s fine — the value isn’t that it’s universally right, it’s that it’s a fixed point. I know exactly what’s in it and exactly what I’d change, and that delta is always small. The alternative — starting from whatever the last project happened to look like — is where the hour goes.
If you’ve moved your own pipeline to OIDC, has it actually let you delete every long-lived token, or just the npm one — and where did the last stubborn stored credential end up living?
References
- BleepingComputer — New npm supply-chain attack self-spreads to steal auth tokens (Shai-Hulud): bleepingcomputer.com
- GitGuardian — The GhostAction Campaign: 3,325 Secrets Stolen Through Compromised GitHub Workflows: blog.gitguardian.com
- GitHub Changelog — npm trusted publishing with OIDC is generally available (July 31, 2025): github.blog
- Livshitz/ts-scaffold — click Use this template and skip the tax. </content> </invoke>