Universal File WatcherRun any external tool on file save and see its output as inline diagnostics — directly in the editor, no tool-specific plugin required. Inspired by PyCharm's File Watchers, this extension is a thin, generic bridge between VS Code and any command-line linter, type checker, formatter, or custom script. Features
Quick Start1 — Install the extension
2 — Add a watcher to your projectOpen
Save any Configuration ReferenceAll settings live under the
|
| Field | Type | Description |
|---|---|---|
name |
string |
Display name (shown in status bar, output channel, diagnostic source) |
filePattern |
string \| string[] |
Glob pattern(s) for files that trigger this watcher |
command |
string |
Command to run (supports variables) |
outputPattern |
string |
Regex with named capture groups to parse each output line |
Optional watcher fields
| Field | Type | Default | Description |
|---|---|---|---|
severity |
"error"\|"warning"\|"info"\|"hint" |
"error" |
Default severity when not captured by regex |
enabled |
boolean |
true |
Whether this specific watcher is active |
runInShell |
boolean |
true |
Run through system shell (allows \| pipes, env vars) |
cwd |
string |
"${workspaceFolder}" |
Working directory (supports variables) |
env |
object |
{} |
Extra environment variables |
encoding |
string |
"utf8" |
Output encoding (utf8, latin1, …) |
parseStderr |
boolean |
false |
Parse stderr in addition to stdout |
applyTo |
"savedFile"\|"matchedFile"\|"allFiles" |
"matchedFile" |
Which file gets the diagnostics (see below) |
continuationPattern |
string |
— | Regex for context lines printed after the main diagnostic. Matched lines are appended to the previous diagnostic's message. Supports a message named group. |
ignoreExitCodes |
number[] |
[] |
Exit codes that are expected (e.g. [1] for linters that exit with 1 on warnings). Others produce a warning in the output channel. |
codeUrl |
string |
— | URL template for error code docs. Use ${code} as placeholder — the code in the Problems panel becomes a clickable link. |
Other settings
| Setting | Type | Default | Description |
|---|---|---|---|
universalFileWatcher.enabled |
boolean |
true |
Master on/off switch for all watchers |
universalFileWatcher.runOnOpen |
boolean |
false |
Also run when a file is first opened |
universalFileWatcher.clearDiagnosticsOnSave |
boolean |
true |
Clear previous diagnostics before each run |
universalFileWatcher.showOutputOnError |
boolean |
false |
Auto-reveal output channel when errors are found |
universalFileWatcher.debounceMs |
number |
300 |
Milliseconds to wait after save before running |
Variables
The following variables are expanded in command and cwd:
| Variable | Expands to |
|---|---|
${file} |
Absolute path of the saved file |
${fileBasename} |
Filename with extension (main.py) |
${fileBasenameNoExtension} |
Filename without extension (main) |
${fileDirname} |
Directory containing the file |
${fileExtname} |
Extension including dot (.py) |
${fileRelative} |
Path relative to workspace root |
${workspaceFolder} |
Absolute path of the workspace root |
Named Capture Groups
The outputPattern regex must use named capture groups ((?<name>...)).
Supported group names
| Group | Required | Description |
|---|---|---|
line |
yes | Line number (1-based) |
col / column |
no | Column number (1-based) |
endLine |
no | End of range line (1-based) |
endCol |
no | End of range column (1-based) |
severity |
no | error, warning, note, info, hint (overrides watcher default) |
message |
yes | Human-readable description |
code |
no | Error code (e.g. E501, TS2304) — shown in Problems panel |
file |
no | File path — used when applyTo is matchedFile or allFiles |
applyTo
| Value | Behaviour |
|---|---|
savedFile |
All diagnostics go to the file that was just saved |
matchedFile |
Diagnostics go to the file in the file capture group (falls back to saved file) |
allFiles |
Diagnostics go to the captured file and the saved file |
Use savedFile when the tool only checks one file at a time (mypy ${file}).
Use matchedFile when the tool checks the whole project and reports many files (tsc --noEmit, cargo check).
Examples
Python — mypy
{
"name": "mypy",
"filePattern": "**/*.py",
"command": "mypy --show-column-numbers --no-error-summary ${file}",
"outputPattern": "^(?<file>[^:]+):(?<line>\\d+):(?<col>\\d+): (?<severity>error|warning): (?<message>.+?)(?:\\s+\\[(?<code>[a-z-]+)\\])?$",
"continuationPattern": "^(?<file>[^:]+):(?<line>\\d+):(?<col>\\d+): note: (?<message>.+)$",
"codeUrl": "https://mypy.readthedocs.io/en/stable/error_codes.html#code-${code}",
"ignoreExitCodes": [1]
}
Python — flake8
{
"name": "flake8",
"filePattern": "**/*.py",
"command": "flake8 ${file}",
"outputPattern": "^(?<file>[^:]+):(?<line>\\d+):(?<col>\\d+): (?<code>[A-Z]\\d+) (?<message>.+)$",
"severity": "warning"
}
Python — ruff
{
"name": "ruff",
"filePattern": "**/*.py",
"command": "ruff check --output-format=text ${file}",
"outputPattern": "^(?<file>[^:]+):(?<line>\\d+):(?<col>\\d+): (?<code>[A-Z]\\d+) (?<message>.+)$",
"severity": "warning",
"codeUrl": "https://docs.astral.sh/ruff/rules/${code}",
"ignoreExitCodes": [1]
}
TypeScript — tsc (whole project)
{
"name": "tsc",
"filePattern": ["**/*.ts", "**/*.tsx"],
"command": "npx tsc --noEmit --pretty false",
"outputPattern": "^(?<file>[^(]+)\\((?<line>\\d+),(?<col>\\d+)\\): (?<severity>error|warning) TS(?<code>\\d+): (?<message>.+)$",
"cwd": "${workspaceFolder}",
"applyTo": "matchedFile"
}
JavaScript/TypeScript — ESLint
{
"name": "eslint",
"filePattern": ["**/*.js", "**/*.ts", "**/*.jsx", "**/*.tsx"],
"command": "npx eslint --format=unix ${file}",
"outputPattern": "^(?<file>[^:]+):(?<line>\\d+):(?<col>\\d+): (?<message>.+) \\[(?<severity>Error|Warning)/(?<code>.+)\\]$",
"severity": "warning",
"codeUrl": "https://eslint.org/docs/rules/${code}",
"ignoreExitCodes": [1]
}
Rust — cargo check
{
"name": "cargo check",
"filePattern": "**/*.rs",
"command": "cargo check --message-format=short 2>&1",
"outputPattern": "^(?<file>[^:]+):(?<line>\\d+):(?<col>\\d+): (?<severity>error|warning)(?:\\[(?<code>[^\\]]+)\\])?: (?<message>.+)$",
"cwd": "${workspaceFolder}",
"applyTo": "matchedFile"
}
Shell — shellcheck
{
"name": "shellcheck",
"filePattern": ["**/*.sh", "**/*.bash"],
"command": "shellcheck -f gcc ${file}",
"outputPattern": "^(?<file>[^:]+):(?<line>\\d+):(?<col>\\d+): (?<severity>error|warning|note): (?<message>.+?) \\[(?<code>SC\\d+)\\]$",
"severity": "warning",
"codeUrl": "https://www.shellcheck.net/wiki/${code}"
}
Custom script
{
"name": "my-checker",
"filePattern": "**/*.py",
"command": "python ${workspaceFolder}/scripts/check.py ${file}",
"outputPattern": "^(?<file>[^:]+):(?<line>\\d+):(?<col>\\d+): (?<severity>error|warning): (?<message>.+)$",
"cwd": "${workspaceFolder}",
"env": {
"MY_API_KEY": "...",
"PYTHONPATH": "${workspaceFolder}/src"
}
}
More examples are in the examples/ directory.
Commands
Open the Command Palette (Ctrl+Shift+P / Cmd+Shift+P):
| Command | Description |
|---|---|
File Watcher: Run on Current File |
Immediately run all matching watchers on the active editor |
File Watcher: Clear All Diagnostics |
Remove all diagnostics produced by this extension |
File Watcher: Enable |
Enable all watchers for this workspace |
File Watcher: Disable |
Disable all watchers for this workspace |
File Watcher: Show Output Channel |
Open the raw command output panel |
Development
Prerequisites
- Node.js 20+
- VS Code 1.85+
Setup
git clone https://github.com/VictorMalyshkin/vscode-universal-file-watcher
cd vscode-universal-file-watcher
npm install
Compile and run
npm run compile # one-time build
npm run watch # watch mode (recompiles on change)
Press F5 in VS Code to open an Extension Development Host with the extension loaded.
Lint
npm run lint
Package as .vsix
npm run package
# produces universal-file-watcher-1.0.0.vsix
Install locally:
code --install-extension universal-file-watcher-1.0.0.vsix
Troubleshooting
No diagnostics appear
- Open File Watcher: Show Output Channel and check the raw output.
- Verify the command runs correctly in your terminal with the exact file path.
- Check that
outputPatternmatches your tool's actual output format — test in regex101.com with the ECMAScript flavor. - Make sure
filePatternmatches the file you saved (e.g.,**/*.pyrequires the path to end in.py).
Command not found
- Set
"runInShell": true(default) so the shell PATH is inherited. - Or use the full path:
"command": "/usr/local/bin/mypy ${file}". - For virtualenv/conda: prefix the command, e.g.
"command": "source .venv/bin/activate && mypy ${file}".
Diagnostics on wrong file
- If the tool checks the whole project (e.g.,
tsc --noEmit), set"applyTo": "matchedFile"and make sure thefilecapture group is in your regex. - If the tool only checks one file, use
"applyTo": "savedFile"(default).
Too slow / running too often
- Increase
universalFileWatcher.debounceMs(default 300 ms). - Disable watchers you don't need with
"enabled": false.
Contributing
Pull requests and issues are welcome. Please open an issue first for significant changes.