EOF Normalizer is a tiny Node.js CLI tool that normalises endβofβfile newlines, converts CRLF β LF, and removes unwanted trailing blank lines.
Whether you're cleaning up formatting inconsistencies, fixing line ending drift in mixedβOS teams, or dealing with editors that add extra blank lines (such as the Cursor editor issue), this tool helps keep your codebase consistent.
If you've ever seen noisy diffs like "+1 blank line" or CI churn because of line ending drift, this tool is for you.
- β
Converts Windows line endings CRLF (
\r\n) β LF (\n) - β Ensures exactly one newline at EOF
- β Removes extra blank lines at end of file
- β Recursively scans a folder
- β
Skips common directories (e.g.
node_modules,.git, build outputs) - β
Respects
.gitignorefiles (automatically skips binaries, generated files, etc.) - β Supports dryβrun mode (preview without changes)
- β Works with any language (TS/JS/Python/Go/Rust/C#/C++/Pascal/Vue/etc.)
- π§Ή Clean diffs before commits (remove unwanted formatting changes)
- π¦ Enforce newline consistency in CI
- π MixedβOS repos: Windows + macOS + Linux teams
- π§ Fix editors that add extra blank lines (including Cursor editor)
- π οΈ General formatting/normalisation utility for any project
- Node.js (recent version recommended)
- Download: https://nodejs.org (LTS is ideal)
- No dependencies required (built-in Node APIs only)
- Optional: Install
ignorepackage for enhanced gitignore pattern matching:(Works fine without it, but thenpm install ignore
ignorepackage provides more accurate gitignore parsing)
- Optional: Install
You only need:
eof-normalizer.js
node eof-normalizer.js --dry-runnode eof-normalizer.jsIf you put the script in a different directory (e.g., tools/ or a shared location), you can specify the directory to scan:
# Script in tools/, scanning src/ directory
node tools/eof-normalizer.js --dir src --dry-run
# Script in parent directory, scanning current directory
node ../eof-normalizer.js --dir . --dry-run
# Using a .gitignore from the scanned directory
node tools/eof-normalizer.js --dir src --gitignore src/.gitignore --dry-run
# Paths with spaces must be quoted
node "C:/My Tools/eof-normalizer.js" --dir "My Project/src" --dry-runNote: Replace tools/eof-normalizer.js with your script path, src with your target directory, and src/.gitignore with the path to the .gitignore file in the directory you're scanning.
node eof-normalizer.js [options]| Option | Description |
|---|---|
--dir <path> |
Directory to scan (default: current directory) |
--ext <list> |
File extensions to process (e.g. .ts,.js,.py) |
--skip <list> |
Folders to skip (comma-separated) |
--gitignore <file> |
Path to .gitignore file (default: ./.gitignore if exists) |
--no-gitignore |
Disable gitignore filtering |
--include-no-ext |
Include files with no file extension (e.g., LICENSE, README) |
--dry-run |
Preview changes without modifying files |
--quiet |
Minimal output (useful for CI) |
--help |
Show help |
node eof-normalizer.jsnode eof-normalizer.js --dir src --ext .ts,.tsx,.js,.jsx,.vuenode eof-normalizer.js --dir . --ext .cs,.csxnode eof-normalizer.js --dir . --ext .c,.cpp,.h,.hpp,.hxx,.cxx,.ccnode eof-normalizer.js --dir . --ext .gonode eof-normalizer.js --dir . --ext .rsnode eof-normalizer.js --dir . --ext .pas,.pp,.pnode eof-normalizer.js --ext .py,.md,.yml,.yaml,.json# Automatically uses ./.gitignore if it exists
node eof-normalizer.js
# Use a custom gitignore file
node eof-normalizer.js --gitignore .myignore
# Disable gitignore filtering
node eof-normalizer.js --no-gitignoreNote about gitignore support: The .gitignore feature was added to help distinguish source files from generated files, binaries, and build artifacts across all project types (C#, C++, Go, Rust, Python, JavaScript, etc.). Since .gitignore files are language-agnostic and commonly used to exclude non-source files, this provides a universal way to identify which files should be normalized.
The built-in parser handles most common patterns, but has some limitations (particularly with complex negation patterns). If you need more accurate gitignore matching, you can install the optional ignore package:
npm install ignoreThe tool will automatically detect and use the ignore package if it's available, providing more robust pattern matching that closely follows git's behavior.
By default, files without extensions (like LICENSE, README, Dockerfile) are not processed. Use --include-no-ext to include them:
# Include files with no extension (e.g., LICENSE, README)
node eof-normalizer.js --include-no-ext --dry-runSample output:
Starting EOF normalization...
π DRY RUN MODE - No files will be modified
Using .gitignore rules (built-in parser): C:\Dev\MyProject\.gitignore
Scanning directories...
C:\Dev\MyProject: found 8 files
Total files to scan: 8
Processing files...
--- Results ---
Would fix 1 file(s):
π LICENSE (1089 β 1056 bytes, -6 lines removed)
Would fix 1 of 8 files.
When scanning a directory outside your current location, you can specify both the target directory and its .gitignore file:
node eof-normalizer.js --dir "C:\Dev\MyProject" --gitignore "C:\Dev\MyProject\.gitignore" --dry-runSample output:
Starting EOF normalization...
π DRY RUN MODE - No files will be modified
Using .gitignore rules (built-in parser): C:\Dev\MyProject\.gitignore
Scanning directories...
C:\Dev\MyProject: found 2761 files
Total files to scan: 2761
Processing files...
--- Results ---
Would fix 100 file(s):
π C:\Dev\MyProject\.github\renovate.json (2324 β 2257 bytes, +1 line)
π C:\Dev\MyProject\.github\scripts\check-schema-drift.sh (1765 β 1764 bytes, -1 line removed)
π C:\Dev\MyProject\.github\workflows\ci.yml (14799 β 14797 bytes, -2 lines removed)
π C:\Dev\MyProject\.secrets\dev\.gitkeep (56 β 53 bytes, -1 line removed)
π C:\Dev\MyProject\.secrets\prod\.gitkeep (56 β 53 bytes, -1 line removed)
π C:\Dev\MyProject\.secrets\README.md (2015 β 2014 bytes, -1 line removed)
π C:\Dev\MyProject\.serena\.gitignore (8 β 7 bytes)
... (93 more files)
Would fix 100 of 2761 files.
First, preview what would be changed:
node eof-normalizer.js --dry-runAfter reviewing the results, apply the changes:
node eof-normalizer.jsThese are examples only β how you use or integrate this tool is entirely your choice.
You've got two common approaches:
Create .git/hooks/pre-commit (no file extension) with:
#!/usr/bin/env bash
set -euo pipefail
# Normalise files
node eof-normalizer.js --quiet
# Fail commit if anything changed (forces you to stage the changes)
if ! git diff --quiet; then
echo "EOF Normalizer updated files. Please review and stage changes, then re-commit."
git status --porcelain
exit 1
fiMake it executable:
chmod +x .git/hooks/pre-commitNotes
- This runs locally only (hooks are not committed by default).
- It prevents βhiddenβ formatting changes slipping into commits.
Install Husky:
npm i -D husky
npx husky initAdd a pre-commit hook:
npx husky add .husky/pre-commit "node eof-normalizer.js --quiet && git diff --quiet || (echo 'EOF Normalizer updated files. Stage changes and retry.' && git status --porcelain && exit 1)"Now the hook is committed and shared with the team.
This workflow fails CI if EOF Normalizer would change any files.
Create .github/workflows/eof-normalizer.yml:
name: EOF Normalizer
on:
pull_request:
push:
branches: [ main, master ]
jobs:
eof-normalizer:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: "20"
- name: Run EOF Normalizer
run: |
node eof-normalizer.js --quiet
- name: Fail if files changed
run: |
git diff --exit-codeHow it works
- Runs the normalizer
- If it modified anything,
git diff --exit-codereturns nonβzero and CI fails - The PR author then runs the tool locally and commits the normalised changes
If you use Cursor and want to trigger EOF Normalizer as part of an MCP workflow, you can run it via an MCP server. This allows Cursor to invoke the existing CLI script programmatically.
Note: This integration has not been formally tested in this repository. The example below shows the intended and theoretical approach based on Cursor's MCP model and is provided as a starting point rather than a supported feature.
For the latest and most accurate MCP implementation details, see the official Cursor documentation:
π https://cursor.com/docs/context/mcp
You would typically create a small MCP server wrapper that shells out to the existing CLI script. For example:
mcp/
βββ package.json
βββ mcp-server.js
The MCP server would:
- Accept requests from Cursor
- Execute
node eof-normalizer.jswith the supplied arguments - Return a summary of the result
This repository intentionally does not include a full MCP server implementation, as MCP APIs and best practices may evolve. Refer to the Cursor docs for concrete implementation guidance.
The following example demonstrates how EOF Normalizer could be registered as an MCP server in Cursor. Paths are anonymised and must be adjusted to match your local setup:
{
"mcpServers": {
"eof-normalizer": {
"command": "node",
"args": [
"/PATH/TO/mcp/mcp-server.js"
],
"env": {
"NODE_ENV": "production",
"EOF_NORMALIZER_SCRIPT": "/PATH/TO/EofNormalizer/eof-normalizer.js"
}
}
}
}This configuration is illustrative only and has not been validated end-to-end.
- Shows how the tool fits into modern Cursor workflows
- Provides a concrete reference without over-specifying implementation
- Leaves flexibility for users to adapt the approach to their own MCP setup
MCP integration is community-driven. If you build and refine an MCP wrapper for this tool, contributions or examples are welcome β but this repository does not aim to provide official MCP support.
If you're modifying or contributing to this script, follow these steps to ensure code quality:
Check for linting errors:
pnpm lintOr with npm:
npm run lintRun all unit tests:
node eof-normalizer.test.jsBefore submitting changes, ensure:
- β
All linting checks pass (
pnpm lint) - β
All unit tests pass (
node eof-normalizer.test.js) - β New features include corresponding unit tests
- β No linter errors are introduced
| File | Purpose |
|---|---|
eof-normalizer.js |
The CLI utility (most users only need this) |
eof-normalizer.test.js |
Tests |
README.md |
Documentation |
LICENSE |
MIT |
package.json |
Dev tooling |
eslint.config.js |
Lint config |
.cursor/rules/javascript-coding-standards.mdc |
Cursor IDE coding standards rules |
.gitignore |
Git hygiene |
- Use
--dry-runfirst - Commit / back up before applying changes
- Provided "as is" under the MIT license (no warranty)
This project is provided as-is as a free utility.
- The maintainer may not be able to reproduce or fix all platform-specific issues
- Edge cases are best handled via community discussion and pull requests
- Contributions are welcome and encouraged
If you need guaranteed support, consider maintaining a fork.
PRs are welcome, but may not be merged immediately (or at all).
This project prioritises simplicity and low maintenance. Changes that add complexity, dependencies, or platform-specific behaviour may be declined.
Cursor blank lines, Cursor adds blank lines, EOF newline, end-of-file newline, newline normalizer, CRLF to LF, line endings, trailing whitespace, trailing blank lines, formatting tool, pre-commit hook, GitHub Actions CI check.
MIT β see LICENSE.