From armor
Sets up TypeScript projects with standardized tooling: oxlint, oxfmt, strict tsconfig, and Knip dead-code detection. Detects existing configs and package managers.
How this skill is triggered — by the user, by Claude, or both
Slash command
/armor:sowThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
You help set up TypeScript projects with standardized tooling: oxlint for linting, oxfmt for formatting, and strict TypeScript configuration. Type-aware linting is available but off by default — it is an opt-in (see "Type-Aware Linting" below).
You help set up TypeScript projects with standardized tooling: oxlint for linting, oxfmt for formatting, and strict TypeScript configuration. Type-aware linting is available but off by default — it is an opt-in (see "Type-Aware Linting" below).
Dev dependencies to install:
oxlintoxfmttypescript@j178/prekknipFirst, assess the current state:
emitDecoratorMetadata: true, set "typescript/consistent-type-imports": "off" — oxlint's --fix rewrites value imports to import type and erases the emitted metadata (typescript-eslint#10200).node_modules? oxlint walks the working tree and obeys .gitignore. Without node_modules ignored, the default lint command hangs trying to lint every dependency.knip.json, knip.jsonc, .knip.json, knip.ts, package.json#knip, depcheck, ts-prune, or similar tools. If Knip already exists, preserve it and tune from findings rather than replacing it.Detect from lockfiles:
bun.lockb or bun.lock → bunpnpm-lock.yaml → pnpmyarn.lock → yarnpackage-lock.json → npmIf no lockfile exists or multiple are present, ask the user which they prefer.
Using the detected package manager:
# bun
bun add -d oxlint oxfmt typescript @j178/prek knip
# pnpm
pnpm add -D oxlint oxfmt typescript @j178/prek knip
# yarn
yarn add -D oxlint oxfmt typescript @j178/prek knip
# npm
npm install -D oxlint oxfmt typescript @j178/prek knip
Copy configs from the references/ folder:
references/oxlintrc.json → .oxlintrc.jsonreferences/oxfmtrc.json → .oxfmtrc.jsonreferences/tsconfig.json → tsconfig.jsonLint config posture. Deliberately loud, and the "off" rules are intentional — they kill formatter conflicts, wrong-environment rules, two mutually-exclusive pairs, and cross-plugin double-reporting. Don't re-enable an off without checking why it's off. One invisible trap: plugins overwrites oxlint's defaults instead of extending them, so the list must name every default too (eslint, typescript, unicorn, oxc) — drop oxc and you silently lose its rules.
For tsconfig, adjust the @src/* path mapping to match the project structure. The config has no baseUrl — TypeScript 6.0 made it a hard error, so paths values are relative (./src/*) and resolve from the tsconfig's location.*
* Version floors. Relative
pathswithoutbaseUrlworks on TS 4.1+. This config's real floor is higher:lib: ["ES2023"]needs TS 5.0+, andmoduleResolution: nodenextneeds 4.7+. DroppingbaseUrldid not lower compatibility — the old config already required 5.0+. To support an older TS, lowerlib(e.g.ES2022reaches back to 4.7); the relativepathsare fine down to 4.1.
The project must have a .gitignore that ignores node_modules (also ignore build output: dist/, build/, coverage/, and .env). oxlint obeys .gitignore to decide what to skip. Without node_modules ignored, oxlint walks the entire dependency tree and hangs. If a .gitignore exists, append the missing entries; otherwise create one.
Merge scripts from references/package-scripts.json into the project's package.json.
Adjust the script runner prefix based on package manager:
bun run check, or just bun checkpnpm checknpm run checkIf eslint, prettier, or similar exists, ask the user before removing:
I found existing [eslint/prettier/etc] configuration. Would you like me to:
- Remove it and use oxlint/oxfmt instead
- Keep it alongside the new tooling
- Skip linting/formatting setup
If they choose to remove, delete:
.eslintrc*, eslint.config.*.prettierrc*, prettier.config.*Type-aware rules are off by default and not in .oxlintrc.json. They are powerful but still alpha: they need a second binary (oxlint-tsgolint, a Go backend that embeds the TypeScript compiler), are slower, and can use a lot of memory on large repos. Leaving them out keeps the default lint honest — every rule in the base config actually runs.
The --tsconfig flag on the default lint script does not enable these rules. It only points oxlint's import plugin at the tsconfig for path-alias resolution. Type-aware rules require a separate flag and binary.
To turn type-aware linting on for a project:
Install the backend:
<pkg-manager> add -D oxlint-tsgolint
Add the rules to .oxlintrc.json:
"typescript/no-floating-promises": "error",
"typescript/no-misused-promises": "error",
"typescript/no-unnecessary-type-assertion": "error",
"typescript/no-unnecessary-condition": "error",
"typescript/prefer-nullish-coalescing": "error",
"typescript/prefer-optional-chain": "error"
prefer-nullish-coalescing and prefer-optional-chain belong here, not in the base config: both need type information to know whether an operand is nullable, so under plain oxlint they silently do nothing.
Add --type-aware to the existing lint script so there is still one lint command, and it's the strong one:
"lint": "oxlint --tsconfig tsconfig.json --type-aware"
Don't ship a separate lint:type-aware script — a second command just means people run the weaker one by reflex and feel covered when they aren't. Make lint the full check.
--type-aware turns on the typescript plugin's whole type-aware rule set, not only the six you listed. Findings will surface from rules you never wrote down — commonly typescript/unbound-method and typescript/no-redundant-type-constituents. Decide each per project: fix the code, or set the rule "off" with a reason. (Example: unbound-method fires on every destructured method, so a codebase whose core pattern is const { method } = ctx turns it off rather than fight its own idiom.)
The pre-commit hook can stay syntactic-only (type-aware is too slow for the inner loop). Run the full type-aware lint in CI, where it's the real gate. That fast/slow split is local-vs-CI, not two scripts a developer juggles.
Without oxlint-tsgolint installed, --type-aware fails fast with: Failed to find tsgolint executable. You may need to add the 'oxlint-tsgolint' package to your project? — so it never silently no-ops.
The oxlint config includes complexity guardrails as warnings with high defaults — they're meant to catch egregious cases, not nag on normal code:
| Rule | Default | What it limits |
|---|---|---|
complexity | 20 | Cyclomatic complexity per function |
max-params | 6 | Function parameters |
max-depth | 5 | Block nesting depth |
max-statements | 40 | Statements per function |
max-lines-per-function | 150 | Lines per function (skips blanks + comments) |
max-nested-callbacks | 4 | Nested callback depth |
Test files (__tests__/**, *.test.ts, *.spec.ts) are excluded from max-nested-callbacks, max-statements, and max-lines-per-function since describe/it nesting naturally inflates these.
The formatter config should include:
$schema for editor validationprintWidth, semicolons, quotes, trailing commas, line endings)sortImports: true so the formatter owns import ordering (the lint sort-imports rule is off to avoid fighting it)sortPackageJson: true to use Oxfmt's package.json orderingignorePatterns for common generated output directoriesOxfmt also respects .gitignore, skips node_modules and lockfiles by default, and supports .prettierignore for compatibility. Prefer config-level ignorePatterns for new projects.
oxfmt is pre-1.0 (beta). Its formatting output can change between minor versions, which surfaces as a large reformatting diff on an otherwise unrelated change. npm install saves a caret range (^0.53.0), and for a 0.x version a caret only allows patch bumps — so day-to-day installs are stable. The risk is a deliberate minor bump (0.53 → 0.54). When bumping oxfmt, do it in its own commit and run format so the reformat is isolated and reviewable. Pin the exact version ([email protected], no caret) if you want zero drift until you choose to move.
Set up prek for a pre-commit hook that runs linting and formatting.
prek reads pre-commit's config schema. Local commands live under a repo = "local" entry, and each hook needs id, name, language, and entry. The flat [[hooks]] shape some docs show does not parse — prek rejects it with missing field 'repos'.
Create prek.toml in the project root:
[[repos]]
repo = "local"
[[repos.hooks]]
id = "oxlint"
name = "oxlint"
language = "system"
entry = "npx oxlint --tsconfig tsconfig.json"
types_or = ["ts", "tsx", "javascript", "jsx"]
pass_filenames = false
[[repos.hooks]]
id = "oxfmt"
name = "oxfmt"
language = "system"
entry = "npx oxfmt --check"
types_or = ["ts", "tsx", "javascript", "jsx", "json"]
pass_filenames = false
language = "system" runs the command as-is (no managed toolchain), which is what npx needs. pass_filenames = false lets each tool resolve its own file set rather than having staged paths appended. Use types_or (match any) not types (match all) — a file is never both ts and tsx.
Install the git hooks (prek needs a git repo, so run git init first if the project isn't one yet):
git init # only if not already a git repo
npx prek install
Note: prek install exits 0 and writes .git/hooks/pre-commit even when the config is malformed — so a bad config fails silently at commit time, not at install. Verify with npx prek run --all-files after installing.
Add a prepare script so the hook installs automatically after npm install:
"prepare": "prek install || true"
The || true keeps Docker builds alive. prepare runs on every npm ci, but a build context has no .git (and --omit=dev has no prek binary), so a bare prek install would exit non-zero and kill the build. prek has no HUSKY=0 skip, so this is the guard.
Set up Knip to find unused files, exports, dependencies, unlisted dependencies, unresolved imports, and unused binaries.
Knip is project-graph tooling, so the correct configuration depends on the repo's real entry points, framework conventions, generated files, workspace boundaries, public API surface, scripts, and dynamic imports. Do not stamp a large shared Knip config onto every repo. Prefer Knip's defaults first, then tune from actual findings.
The package scripts include:
"knip": "knip",
"knip:production": "knip --production"
Do not create a Knip config by default unless the first run shows false positives or missing coverage. If configuration is needed, create knip.jsonc with the schema and tune only what the project needs:
{
"$schema": "https://unpkg.com/knip@6/schema-jsonc.json",
}
Common tuning:
entryprojectignoreFiles for generated files, fixtures, or examples that should not count as unused filesignoreDependencies, ignoreUnresolved, or ignoreIssues only for known false positivesincludeEntryExports: true for private apps if unused exports from entry files should be reportedincludeEntryExports off for public libraries, where entry-file exports are public APIworkspacesAvoid broad ignore patterns. They hide too much. Prefer more specific configuration or better entry / project coverage.
Do not add Knip to the pre-commit hook. It is a whole-project maintenance check, not a staged-file check. Pre-push or CI enforcement is useful after the initial report is clean or intentionally configured.
Standardize the repo's agent-instructions file and record the dependency policy. AGENTS.md is the canonical file; CLAUDE.md is a symlink to it, so both tools read the same source.
Run the helper from the project root:
bash <skill-path>/scripts/setup-agents-md.sh
It is idempotent and safe to re-run. It handles every starting state:
CLAUDE.md → renames it to AGENTS.md, links CLAUDE.md → AGENTS.mdAGENTS.md → links CLAUDE.md → AGENTS.mdThen it appends the dependency policy to AGENTS.md, once, guarded by a <!-- sow:dependency-policy --> marker so re-runs don't duplicate it. The policy also tells future agents to run Knip before handoff when their changes affect the project graph.
The script doesn't carry that text inline — it appends assets/dependency-policy.md verbatim. That file is the exact content written to AGENTS.md. Read it to see what gets appended; edit it to change the policy. There is one copy, so nothing can drift.
On a fresh setup, normalize formatting first — an existing codebase has not been run through oxfmt, so format:check would fail on every unformatted file and tell you nothing useful:
# Normalize formatting once (writes changes)
<pkg-manager> run format
Then verify:
# Type check
<pkg-manager> run check
# Lint
<pkg-manager> run lint
# Format check (should pass now)
<pkg-manager> run format:check
# Dead code and dependency hygiene audit
<pkg-manager> run knip
Read the results by kind, they are not the same signal:
format:check failures mean files need formatting. The fix is running format — not a code problem. This is why you normalize first.check (tsc) and lint failures are real: existing code that does not meet the stricter type and lint standards. Report these to the user; they may need code changes, not just a reformat.knip failures are project-graph findings. For fresh projects, fix them immediately. For existing projects, classify the findings: fix obvious issues caused by setup, tune precise Knip config for false positives, and ask before deleting unrelated files, removing dependencies, or changing public exports.In CI, run format:check (not format) so the build fails on unformatted code instead of silently rewriting it.
npx claudepluginhub markacianfrani/armor --plugin armorCreates bite-sized, testable implementation plans from specs or requirements, with file structure and task decomposition. Activates before coding multi-step tasks.