Let Claude Code hear your type errors as they happen: a tsc monitor that only speaks when something changes
A plugin monitor can pipe `tsc --watch` into a running Claude Code session. Raw, it's noise. Filtered to the delta, it's a verification loop the agent can't forget to run. About 15 minutes to build.

An agent renames an exported function in price.ts. Four files import it. The agent edits two of them, says the job is done, and the type error in cart.ts surfaces eleven minutes later when CI goes red. The agent could have caught it. It just wasn’t told, because nothing in the session was watching the compiler.
The usual patch is a line in CLAUDE.md: “run tsc --noEmit after every change”. That works until the agent is deep in a long task and forgets, or runs it once at the end and drowns in forty errors from a half-finished refactor.
Claude Code has a better channel for this. The Monitor tool lets a background process feed lines into the running session, and a plugin can declare a monitor that starts with the session. Point one at tsc --watch and the agent hears about each new type error within a couple of seconds of causing it, in the middle of its own work, without anyone asking.
Pointed at raw tsc --watch, though, it’s a firehose. The build below puts a small filter between the compiler and the agent so the session hears only what changed. It’s about 70 lines of Python and four small files, and it needs a TypeScript project and Claude Code.
What the Monitor tool does
According to the tools reference
, Monitor runs a command in the background and delivers each line it prints to Claude as it arrives. Claude keeps working and reacts when an event lands. A watch Claude starts itself has a deadline: 5 minutes by default, 30 at most, and 10 at most in a non-interactive -p run. Monitor commands go through the same permission rules as Bash.
A monitor declared by a plugin is different in one way that matters here. Per the plugin components docs
, it’s a shell command that Claude Code starts at session start and keeps running until the session ends. No deadline, and nobody has to remember to ask. The same page lists the limits: plugin monitors start in interactive sessions only, never with -p, and they run with your full user permissions, outside the sandbox. That second point decides how carefully you read the script before installing it.
Why not pipe tsc straight in
Run tsc --watch --noEmit --pretty false --preserveWatchOutput and every recompile prints a start line, every current error, and a summary line. Edit five times in a project with two errors and the same errors are printed again and again. Each printed line is something the agent has to read, and each notification can wake it up for a turn it didn’t need.
Here’s a measured comparison. The same five edits (a rename that breaks an import, a save with no change, two lines added above the broken import, a second error, then fixing both) produced 29 lines of raw watch output. The filtered version below printed 6.
The filter’s whole job is a diff. It collects the errors from each compile, compares them with the previous compile, and prints only what’s new or gone. Two details make that diff trustworthy:
- Errors are keyed by file, code and message, plus an occurrence count, and not by line number. Add two comment lines above an existing error and it shifts down two lines. It’s still the same error, so nothing is printed.
- The first compile is a baseline. Existing errors are listed once (up to five), so the agent knows what it inherited, and after that only changes are reported.
Build it
Make a folder for the plugin, anywhere outside the project:
mkdir -p tsc-delta/.claude-plugin tsc-delta/monitors tsc-delta/scripts
The manifest needs only a name. A version and description make /plugin readable:
{
"name": "tsc-delta",
"version": "1.0.0",
"description": "Streams new TypeScript errors to Claude as they appear, and says so when they clear"
}
Save that as tsc-delta/.claude-plugin/plugin.json. Then declare the monitor in tsc-delta/monitors/monitors.json:
[
{
"name": "tsc-delta",
"command": "python3 \"${CLAUDE_PLUGIN_ROOT}/scripts/tsc-delta.py\" -- npx --no-install tsc --watch --noEmit --pretty false --preserveWatchOutput",
"description": "New TypeScript errors"
}
]
The command runs in a shell in the session’s working directory, so npx finds the project’s own TypeScript. --no-install stops it fetching a compiler from the network if the project doesn’t have one, and --pretty false plus --preserveWatchOutput keep the output free of colour codes and screen-clearing escapes (without the second flag, a watching tsc clears the terminal on each compile). ${CLAUDE_PLUGIN_ROOT} is the plugin’s install directory, and the quotes keep a path with spaces as one word.
Now the filter, saved as tsc-delta/scripts/tsc-delta.py:
#!/usr/bin/env python3
"""Run `tsc --watch` and print only what changed between compiles.
Each line printed to stdout reaches Claude as one notification, so the goal is
silence unless the set of errors changes. Errors are keyed by file, code,
message and occurrence count, not by line number, so adding a line above an old
error doesn't make it look new, and seven identical errors stay seven.
"""
import re
import subprocess
import sys
MAX_LISTED = 5
ERROR = re.compile(r"^(?P<file>.+?)\((?P<line>\d+),(?P<col>\d+)\): error (?P<code>TS\d+): (?P<msg>.*)$")
SUMMARY = re.compile(r"Found (\d+) errors?\.")
def say(text: str) -> None:
print(text, flush=True)
def main() -> int:
if "--" not in sys.argv:
print("usage: tsc-delta.py -- <command that runs tsc --watch>", file=sys.stderr)
return 2
cmd = sys.argv[sys.argv.index("--") + 1:]
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
seen = None # error keys from the previous compile; None before the first
current = {} # key -> display line, for the compile in progress
counts = {} # how often each file/code/message has appeared in this compile
for raw in proc.stdout:
line = raw.rstrip("\n")
m = ERROR.match(line)
if m:
base = (m["file"], m["code"], m["msg"])
nth = counts[base] = counts.get(base, 0) + 1
key = base + (nth,)
current[key] = f'{m["file"]}:{m["line"]} {m["code"]} {m["msg"]}'
continue
if not SUMMARY.search(line):
continue
if seen is None:
say(f"tsc watching. Baseline: {len(current)} existing error(s). Later notices list only changes.")
for text in list(current.values())[:MAX_LISTED]:
say(f" {text}")
if len(current) > MAX_LISTED:
say(f" ...and {len(current) - MAX_LISTED} more")
else:
added = [current[k] for k in current if k not in seen]
fixed = [k for k in seen if k not in current]
if added:
say(f"tsc: {len(added)} new error(s), {len(current)} total")
for text in added[:MAX_LISTED]:
say(f" {text}")
if len(added) > MAX_LISTED:
say(f" ...and {len(added) - MAX_LISTED} more")
elif fixed and not current:
say(f"tsc: clean, {len(fixed)} error(s) fixed")
elif fixed:
say(f"tsc: {len(fixed)} error(s) fixed, {len(current)} left")
seen = set(current)
current = {}
counts = {}
code = proc.wait()
say(f"tsc watcher exited with code {code}; no more type-check notices until it is restarted.")
return code
if __name__ == "__main__":
sys.exit(main())
A few choices in there are deliberate. The Found N errors. line is the end-of-compile marker, so the script waits for it before comparing anything. A compile that finds nothing new prints nothing at all, which is what keeps the agent’s context clean. The “clean” line matters as much as the error lines: it’s the agent’s signal that a refactor has landed.
The last line matters too. If tsc dies (a broken tsconfig.json, say), the monitor would otherwise go quiet, and silence looks exactly like “no errors”. The exit notice tells the agent the checker is gone.
Validate the plugin before loading it:
claude plugin validate ./tsc-delta
It should pass. Expect one warning about a missing icon, which only matters if you publish the plugin to a directory.
Load it and watch it work
From the root of a TypeScript project:
claude --plugin-dir /path/to/tsc-delta
--plugin-dir loads a plugin for that session only, which makes it the right way to try this without installing anything. The monitor starts with the session. If you edit the plugin afterwards, /reload-plugins picks up the change.
Now give the agent a task that crosses files. A rename is ideal:
Rename the exported function `total` in src/price.ts to `sum`, and update everything that depends on it.
The filter’s own output, run by hand against a small project (price.ts exports total, cart.ts imports it), looks like this when the export is renamed, then another error is added, then both are fixed:
tsc watching. Baseline: 0 existing error(s). Later notices list only changes.
tsc: 1 new error(s), 1 total
src/cart.ts:1 TS2305 Module '"./price"' has no exported member 'total'.
tsc: 1 new error(s), 2 total
src/cart.ts:8 TS2322 Type 'string' is not assignable to type 'number'.
tsc: clean, 2 error(s) fixed
Three change notices came from five edits. The save with no change and the two comment lines added above the broken import produced nothing, because the set of errors hadn’t changed.
An agent receives the same text. A headless claude -p run, started with the Monitor tool on this exact command (plugin monitors don’t start under -p, so the command was launched through the tool instead), was told to rename the function in price.ts only, wait for the monitor’s baseline, then fix whatever it reported. It received three notices, verbatim:
tsc watching. Baseline: 0 existing error(s). Later notices list only changes.
tsc: 1 new error(s), 1 total
src/cart.ts:1 TS2305 Module '"./price"' has no exported member 'total'.
tsc: clean, 1 error(s) fixed
Between the second and third, the agent read cart.ts and changed the import and the call to sum. The error it fixed was one it hadn’t gone looking for.
Make it yours
Start the monitor only when it’s useful. The manifest’s when field takes "always" (the default) or "on-skill-invoke:<skill>", which starts the monitor the first time that skill runs. If a refactoring skill is the only time you want the watcher, attach it there and keep ordinary sessions quiet:
{
"name": "tsc-delta",
"command": "python3 \"${CLAUDE_PLUGIN_ROOT}/scripts/tsc-delta.py\" -- npx --no-install tsc --watch --noEmit --pretty false --preserveWatchOutput",
"description": "New TypeScript errors",
"when": "on-skill-invoke:refactor"
}
Point it at a specific package. In a monorepo the monitor runs in the session’s working directory, so add -p packages/web (tsc’s --project flag) to the tsc arguments after the --. A second entry with a different name watches another package.
Tell the agent what the notices mean. One line in CLAUDE.md is enough: “A tsc: notice means the type-checker disagrees with your last edit. Finish the multi-file change you’re making, then fix what’s reported. Don’t declare a task done until the last notice says clean.” That last sentence is the verification rule, enforced by data rather than by memory.
Swap the compiler. Only the regular expression and the summary marker are tied to tsc. For a C# project the same shape would wrap dotnet watch build and parse MSBuild’s error CS1234 lines, keyed the same way. That variation hasn’t been run here, so check the output format before trusting the regex.
What goes wrong
Half-finished changes look like regressions. An agent renaming across ten files will see errors after the first edit, and that’s correct: the code really doesn’t compile yet. The CLAUDE.md line above is there so the agent finishes the pass before reacting. If it chases every notice mid-refactor, it’ll thrash, so the instruction is the mitigation.
The baseline can race the first edit. If the agent edits a file before the first compile finishes, the “baseline” already contains its own breakage. A script that printed only the count (“1 existing error(s)”) would leave the agent waiting for a later event that never comes, because a baseline error is never reported again. That’s why the script lists the baseline errors, up to five. Telling the agent to wait for the baseline line before its first edit is a sensible habit for any monitor.
Plugin monitors run unsandboxed with your permissions. The command is fixed in monitors.json, so it can’t be changed by a prompt, but the script it runs is yours to audit, and so is any third-party monitor you install. Read the command and every script it calls first. Don’t install a monitor you haven’t read.
Availability has edges. Plugin monitors start in interactive sessions only, so they don’t help in claude -p scripts or CI. On Windows, the Monitor tool needs Git Bash. The tools reference still says the tool is unavailable on Bedrock, Vertex and Foundry and when DISABLE_TELEMETRY or CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC is set, but the 2.1.310 changelog entry (23 October 2026) says it now works in all of those. If your setup is one of them, update Claude Code first and test before relying on it.
The parser reads only the first line of an error. TypeScript prints multi-line diagnostics (long generic mismatches) with indented continuation lines, and the filter drops them. The agent gets the code and the headline, and can run npx tsc --noEmit for the full text when the headline isn’t enough.
Don’t bother if the agent already runs a fast check after every edit. A PostToolUse hook that runs a quick type check gives a result in the same turn as the edit and can feed the failure straight back, which a monitor can’t do. A monitor earns its place when the check is slow, when an edit in one file breaks another that nobody is looking at, or when you want the agent told about errors from your own edits in the same checkout.
It also costs tokens. Every notice that lands is a short message in context, and a noisy project with hundreds of baseline errors will print the same five on each session start. If the baseline is that bad, fix or suppress it before adding a monitor.
The first 15 minutes
Copy the four files into a folder. Run claude plugin validate on it. Open a session in a TypeScript project with claude --plugin-dir, wait for the baseline notice, and ask for a rename that touches three files.
If the notices read the way the sample above does, add the CLAUDE.md line. Tighten MAX_LISTED or the key if your project throws many near-identical errors, and move on to a second monitor for your slowest check.