The interesting thing about pointing an agent at a TypeScript project is that the project already contains a program whose entire job is to find the errors. So the question is not whether Claude Code can find a type error. It is whether tsc tells it anything it did not already know — and what asking costs.
We built a three-file project with three planted errors, one per compiler behaviour class, and ran nine billed arms against it on CLI 2.1.280. Every arm found every error. The compiler output changed nothing about the answer. What it changed was how much the agent read to produce it: the arms that were handed tsc --noEmit output opened no files at all and used a third of the input-side tokens.
Key takeaways
- Three source-only arms found 3 of 3 errors with the shell denied, naming the right file, line and
TSxxxxcode each time — including the one onlystrictreports. - Three arms given the compiler output also found 3 of 3, and read nothing:
31,177input-side tokens against94,352–94,380for the arms that had to look. tsconfig.jsonwas read unprompted on every arm that opened a file at all.- The unrestricted arm was the most expensive route to the same answer — it ran the compiler itself, took 7 turns and consumed
183,357input-side tokens. strict: trueadded exactly one error to the same three files:TS7006. The other two are reported either way.
The short answer
Run the compiler yourself and paste the output. On this project it did not improve what Claude Code found — six arms out of six found everything either way — but it replaced roughly 63,000 tokens of file reading with 297 bytes of text, and it was the cheapest of the three routes we measured. Do not skip strict because the agent seems to cope without it: the agent found the implicit-any regardless, but the compiler only reports it when strict is on, and the compiler is what your CI has.
For the wider setup this sits inside — where CLAUDE.md goes, what the permission system does — the Claude Code CLI overview and our complete Claude Code guide cover the foundation workflow this fixture is a miniature of.
What tsc already knows
The fixture is deliberately small: two source files, a tsconfig.json, and one empty directory. Three errors, chosen because the compiler treats each one differently.
{
"compilerOptions": {
"strict": true,
"noEmit": true,
"moduleResolution": "bundler",
"paths": { "@/*": ["./*"] }
},
"include": ["**/*.ts"]
}
// E1 — no annotation on `value`
export function slugify(value) {
return value.toLowerCase().replace(/[^a-z0-9]+/g, "-");
}
export function repeat(text: string, times: number): string {
return new Array(times + 1).join(text);
}
import { formatDate } from "@/lib/dates"; // E2 — `lib/dates.ts` does not exist
import { repeat, slugify } from "./util";
export function banner(title: string): string {
const rule = repeat("-", "24"); // E3 — string where number is declared
return [formatDate(new Date()), slugify(title), rule].join("\n");
}
Run the compiler and it says all three in 297 bytes:
src/main.ts(3,28): error TS2307: Cannot find module '@/lib/dates' or its corresponding type declarations. src/main.ts(10,28): error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'. src/util.ts(4,25): error TS7006: Parameter 'value' implicitly has an 'any' type.
That is the baseline the agent is being compared against — not a benchmark, a ground truth. The compiler is right by construction here, because we planted the errors.
Does Claude Code read tsconfig.json?
Yes, and without being asked. Even though the TypeScript tsconfig documentation defines dozens of compiler options, the prompt given to every arm was the same and never mentions configuration:
This is a small TypeScript project. List every type error in it. For each one give the file, the line, and the TypeScript error code (TSxxxx). Do not fix anything. Answer with a list and nothing else.
Every arm that opened a file at all opened tsconfig.json — three source-only runs and the unrestricted run, four for four. One of them volunteered the fact in its answer without being asked how it worked:
I didn't run `tsc`. I have no shell tool here, so this list comes from reading `tsconfig.json`, `src/util.ts` and `src/main.ts`.
This is the part that generalises least gracefully to a real repository. A three-file project is small enough to read exhaustively; what actually fills a context window is the measurement for what happens when it is not.
The three errors, and the one strict adds
The three planted errors are not interchangeable. Each one exercises a different part of the compiler (as documented in the TypeScript compiler options handbook), and only one of them depends on strict.
| Code | Class | Reported with strict: false? | |
|---|---|---|---|
| E1 | TS7006 | implicit any | No |
| E2 | TS2307 | module resolution | Yes |
| E3 | TS2345 | assignability | Yes |
Same files, one flag, run live on every check:
node node_modules/typescript/bin/tsc --noEmit -p tsconfig.json # → 3 errors: TS2307 TS2345 TS7006 node node_modules/typescript/bin/tsc --noEmit -p tsconfig.json --strict false # → 2 errors: TS2307 TS2345
slugify is exactly as untyped in both runs. The only thing strict: false changes is whether anyone is told.
What the compiler output actually changed
Two arms, three runs each, shell denied in both so neither could run the compiler itself. Arm A got the source. Arm B got the source and the 297 bytes above.
| Run | A: found | A input-side | A tools | B: found | B input-side | B tools |
|---|---|---|---|---|---|---|
| 1 | 3/3 | 94,354 | Glob, Read | 3/3 | 31,177 | none |
| 2 | 3/3 | 94,380 | Glob, Read | 3/3 | 31,177 | none |
| 3 | 3/3 | 94,352 | Glob, Read | 3/3 | 31,177 | none |
Six runs, six times 3 of 3. Not once did the compiler output cause an error to be found that reading the source did not find, and not once did its absence cause one to be missed. On accuracy, against this fixture, tsc told Claude Code nothing it could not work out.
That is the null result, and it is the honest headline. It is also not the whole of it, because the right-hand column never stopped being different.
The context cost runs the other way
The expectation going in was that pasting compiler output costs context. It does the opposite here, and the reason is in the tools column: arm B read nothing, in all three runs.
- Arm A: ~94,360 input-side tokens, two tool calls, then an answer.
- Arm B: 31,177 input-side tokens — identical to the token in all three runs, because with no file reads the input is deterministic — and no tool calls.
That is 0.33x, a saving of roughly 63,000 input-side tokens, bought with 297 bytes. The output did not add context; it removed the reason to go looking for any. Output tokens moved the same way — 642–745 for arm A against 94–118 for arm B, which transcribed a list it had already been given.
The unrestricted run is the expensive one
The two arms above had the shell denied, which is not how anybody runs this. So the third arm ran with nothing denied and no compiler output supplied — the realistic default.
It did what you would expect: reached for the shell, ran the compiler, and reported the three errors. It took 7 turns and 183,357 input-side tokens.
| Arm | Route to the answer | Input-side | Found |
|---|---|---|---|
| B | handed the output | 31,177 | 3/3 |
| A | read the source | ~94,360 | 3/3 |
| C | ran the compiler itself | 183,357 | 3/3 |
The most autonomous route was the most expensive, by 5.9x over the cheapest, for an identical answer. Letting the agent discover that it should run tsc, run it, and read the result costs more than running it yourself and pasting three lines — on a project with three files. The gap is unlikely to close as the project grows.
What strict: true does and does not do
Two things are worth separating, because the measurement only supports one of them.
Measured: strict: true changes what the compiler reports. Three errors instead of two on identical files, and TS7006 is the whole difference.
Not measured: whether strict: true changes the code Claude Code writes. Nothing in this experiment asked it to generate anything — every arm was told "do not fix anything" precisely so that the finding would be about detection rather than repair. A generation experiment is a different fixture and a different article.
What can be said is narrower and still useful: the agent identified the unannotated parameter in all six restricted runs, including the three where it had no compiler output at all. It did not need strict to see the problem. Your CI does — which is the argument for the flag that survives this result intact, and the one setting up CLAUDE.md is the right place to write down.
What we are not claiming
That this generalises to a real repository. Three files is small enough to read exhaustively; the site this fixture lives in has 77 .ts and .tsx files and tsc --noEmit output that is empty rather than 297 bytes. The 0.33x ratio is this project's, and the direction is more likely to hold than the magnitude.
That the agent cannot be fooled. The errors were planted, obvious and three in number. A subtle variance bug or a type that is wrong rather than missing is a different test, and this one says nothing about it.
That strict does not affect generated code. It was not tested. See above.
That nine arms is a lot. It is three runs of two conditions plus one of a third, on one machine, one account, one day, one model tier at --effort low. The A/B result reproduced three times for three, which is why it is stated as a finding; the arm C figure is a single observation and is stated as one.
That we know why arm B skipped reading. It did, in all three runs, and the token counts prove it. Whether that is a policy, a heuristic or a coincidence of this prompt is not visible from outside and is not claimed.
Common mistakes
- Assuming the compiler output is the expensive part. It was 297 bytes and it saved roughly 63,000 tokens, because it removed the need to explore.
- Letting the agent discover it should run
tsc. It will, and on this fixture that route cost 5.9x the cheapest one for the same three errors. - Reading a green
tsc --noEmitas "no type problems". Withstrict: falsethis project is two-thirds green and exactly as broken — see the table above. - Trusting a check that cannot fail. The first version of the script behind this article shelled out to
npxon Windows, where it is a.cmdshim thatexecFileSynccannot launch; theENOENTlanded in the samecatchas a normal non-zerotscexit and the fixture recorded zero errors for a project with three. The guards caught it because they assert a count. A fixture that only records what it was given would have published the number. - Comparing a cold run to a warm one. Run 1 here cost 4x runs 2 and 3 on the same arm, and none of that difference is about TypeScript.
Conclusion
Run tsc --noEmit yourself and paste the output into the prompt. It will not make Claude Code more accurate — on this fixture nothing did, because it was already finding everything — but it stops the agent reading your repository to rediscover what the compiler computed in a second, and on three files that was the difference between 31,177 tokens and 183,357.
Then keep strict: true for the reason it always existed. The agent found the implicit any without it. The compiler will not, and the compiler is what runs in CI when nobody is watching — the same argument that applies to every other silent failure in a build, and the reason this site has a check for each one it has been bitten by.
We re-run this with an internal check script. The compiler arms re-run live on every invocation; the billed arms are replayed from the committed fixture unless we explicitly re-measure them.
Frequently asked questions
Does Claude Code read tsconfig.json on its own?
Does giving Claude Code tsc output help it find type errors?
How many tokens does tsc --noEmit output cost in context?
Does strict: true change the code Claude Code writes?
Which TypeScript errors does strict: true hide when it is off?
Should I let Claude Code run tsc itself?
Muhammad Kashif
Founder and editor of Devventa, covering AI coding assistants, Next.js and the modern AI development stack.




