A zero-cost local background service that watches your repository for file changes, automatically stages them, generates a conventional commit message by analysing the diff, commits, and pushes to GitHub — with no manual intervention and no API keys required.
git clone https://github.com/trynna-be-nerdy/git-auto-commit.git
cd git-auto-commit
npm install
npm run build
cp auto-commit.json .auto-commit.json # edit if needed
npm startThat's it. The service is now watching the current directory and will commit + push on every save.
For a persistent background process that survives reboots, use pm2 mode instead.
- Fully free — commit messages are generated by a local rule-based diff parser. No AI API, no credits, no network calls.
- Conventional commits — output always follows the
<type>(<scope>): <description>format. - Debounced batching — rapid saves are grouped into one commit instead of spamming your history.
- Safe by default — never force-pushes. On push failure the commit stays local.
- Dry-run mode — preview every action without touching Git.
- pm2 integration — run as a persistent background process that survives reboots.
- Structured logging — JSON log file with automatic 5 MB rotation.
- Watch — chokidar monitors your repo for file saves (
add,change,unlink). - Debounce — rapid saves are batched; a commit fires only after a configurable period of inactivity.
- Stage —
git add -Astages everything not inignorePatterns. - Analyse — a built-in rule-based parser reads the
git diff --cachedoutput and infers a conventional commit message from file types and change patterns. No AI, no network, no API keys. - Commit —
git commit -m "<generated message>". - Push — if
autoPushistrue,git push origin mainruns automatically.
- Node.js 20 LTS or later
- Git installed and a configured remote (
origin) - npm
git-auto-commit/
├── src/
│ ├── index.ts # CLI entry point (--dry-run, --help, signal handlers)
│ ├── autocommit.ts # Orchestrator — ties all modules together
│ ├── watcher.ts # chokidar file watcher with debounce
│ ├── git.ts # simple-git wrapper (stage, diff, commit, push)
│ ├── commit-message.ts # Rule-based conventional commit generator
│ ├── config.ts # Zod schema + .auto-commit.json loader
│ └── logger.ts # JSON logger with 5 MB rotation
├── dist/ # Compiled output (generated by `npm run build`)
├── auto-commit.json # Template config — copy to .auto-commit.json
├── ecosystem.config.cjs # pm2 process config
├── start.ps1 # One-command Windows launcher
├── start.sh # One-command macOS/Linux launcher
├── tsconfig.json
└── package.json
git clone https://github.com/trynna-be-nerdy/git-auto-commit.git
cd git-auto-commit
npm install
npm run build
cp auto-commit.json .auto-commit.json
npm startClone git-auto-commit as a subdirectory of the project you want to watch:
cd my-project # your existing git repo
git clone https://github.com/trynna-be-nerdy/git-auto-commit.git
cd git-auto-commit
npm install
npm run build
cp auto-commit.json ../.auto-commit.json # config goes in the project rootThen start the service from inside the git-auto-commit folder:
npm startThe service automatically detects it is running inside a subdirectory, walks up to the repo root (my-project/), and watches the entire project from there. The git-auto-commit/ folder itself is silently excluded from watching so it never commits its own files.
You will see this on startup:
[auto-commit] Detected subdirectory install — watching repo root: /path/to/my-project
[auto-commit] Auto-ignoring install directory: git-auto-commit
You can also add git-auto-commit/ to your project's .gitignore if you don't want it tracked:
echo "git-auto-commit/" >> ../.gitignoreEdit .auto-commit.json in your repository root. The file is git-ignored so it never gets committed.
{
"watchPaths": ["."],
"ignorePatterns": ["node_modules", "dist", ".git", "*.log", ".auto-commit.log"],
"debounceSeconds": 10,
"autoPush": true,
"remoteName": "origin",
"branch": "main"
}| Field | Default | Description |
|---|---|---|
watchPaths |
["."] |
Directories to watch, relative to the repo root |
ignorePatterns |
see above | Glob patterns and directory names to ignore |
debounceSeconds |
10 |
Seconds of inactivity before a commit is triggered |
autoPush |
true |
Push to remote after each commit |
remoteName |
"origin" |
Git remote to push to |
branch |
"main" |
Branch to push to |
All fields are optional — if .auto-commit.json is missing entirely, the defaults above are used and a warning is printed on startup.
You can also override the config file path with an environment variable:
AUTO_COMMIT_CONFIG=/path/to/custom.json npm startBest for testing. Restarts automatically when you edit source files.
npm run devnpm run build
npm startPrints every action (stage / commit / push) without actually executing any Git commands. Use this to verify the service is working before letting it commit.
npm start -- --dry-run
# or in dev mode:
npm run dev -- --dry-runExample dry-run output:
[auto-commit] Service started — watching: . | debounce: 10s | autoPush: true | DRY-RUN
[auto-commit] Change detected: src/auth/login.ts
[auto-commit] Batch ready (1 file(s) changed).
[dry-run] Would run: git add -A
[auto-commit] Commit message: "feat(src/auth): add login.ts"
[dry-run] Would run: git commit -m "feat(src/auth): add login.ts"
[dry-run] Would run: git push origin main
pm2 keeps the service running in the background and restarts it automatically if it crashes or the machine reboots.
# Start (builds first, then launches via pm2)
npm run pm2:start
# View live logs
npm run pm2:logs
# Restart after config changes
npm run pm2:restart
# Stop the service
npm run pm2:stopAlternatively use the platform scripts:
# Windows
.\start.ps1# macOS / Linux
chmod +x start.sh && ./start.shnpm start -- --helpMessages follow the Conventional Commits specification:
<type>(<scope>): <description>
Type is inferred from the files changed:
| Type | When it's used |
|---|---|
test |
All changed files are test/spec files |
docs |
All changed files are markdown / text |
style |
All changed files are CSS / SCSS / SASS |
chore |
All changed files are config / tooling / lock files |
feat |
Any new source file added |
fix |
Diff contains keywords: fix, fixed, bug, error, patch |
chore |
Fallback for mixed or unclassified changes |
Scope is the longest common parent directory of all changed files (e.g. src/auth). Omitted if files span multiple top-level directories.
Description lists file names by action:
add login.ts, register.tsupdate user.ts, +3 more(capped at 3 names)remove legacy.ts
The full subject line is truncated to 72 characters.
Examples:
feat(src/auth): add login.ts, register.ts
fix(src/api): update handler.ts
chore: update package.json, package-lock.json
test(src/utils): add parser.spec.ts
docs: update README.md
Structured JSON logs are written to .auto-commit.log in the repo root (git-ignored).
{"timestamp":"2026-04-24T10:30:00.000Z","level":"info","message":"Committing: \"feat(src): add index.ts\""}
{"timestamp":"2026-04-24T10:30:01.000Z","level":"info","message":"Push succeeded."}The log file rotates automatically at 5 MB — the previous file is kept as .auto-commit.log.1.
To tail logs when running via pm2:
npm run pm2:logsService starts but no commits are created
- Check
debounceSeconds— the timer resets on every save, so rapid edits keep delaying the commit. - Verify the changed files are not matched by
ignorePatterns. - Run with
--dry-runto confirm the watcher is detecting changes.
Push is rejected
- The remote branch has diverged. The commit is saved locally — pull/rebase manually then push.
- The service will never force-push.
Merge conflict detected — skipping
- Resolve the conflict manually, then the next save will trigger a normal commit cycle.
Nothing staged after git add
- Git sees no diff (e.g. a file was saved without changing content, or changes were already committed). This is normal — the cycle is skipped silently.
pm2 not found
- pm2 is installed as a dev dependency. Use
npx pm2or install globally:npm install -g pm2.
The system is a Node.js TypeScript process composed of five modules that form a linear pipeline: watch → stage → diff → generate message → commit/push.
┌─────────────────────────────────────────────────────────────────┐
│ AutoCommitService │
│ │
│ FileWatcher ──(batch ready)──► GitService ──► CommitMessage │
│ │ │ Generator │
│ │ chokidar │ (rule-based) │
│ │ debounce timer │ │
│ └──────────────────────────────┴──► git commit + push │
└─────────────────────────────────────────────────────────────────┘
│ │
Logger Logger
(.auto-commit.log) (.auto-commit.log)
Reads .auto-commit.json from the repo root (or the path in AUTO_COMMIT_CONFIG). Uses zod to validate the schema and fill in defaults. Returns a strongly-typed Config object consumed by all other modules.
If the file is missing, defaults are used and a warning is logged. If the file is malformed, the process exits with a descriptive error.
Wraps chokidar to watch one or more directories for add, change, and unlink events. Two key behaviours:
- Ignore filtering — plain directory names (e.g.
node_modules) are converted to anchored regexes; glob patterns are passed through to chokidar directly. - Debounce — every file event resets a
setTimeout. Only when the timer expires with no new events does it callonBatchReady(paths[]). This means 50 rapid saves become one commit instead of 50.
awaitWriteFinish is enabled so chokidar waits for the OS file-write to settle before emitting the event — prevents partial-write commits.
Wraps simple-git with four operations:
| Method | What it runs |
|---|---|
hasUnresolvedConflicts() |
git status --porcelain — checks for UU/AA/DD markers |
stageAll() |
git add -A |
getStagedDiff(maxTokens) |
git diff --cached — truncates at the character limit if needed |
commit(message) |
git commit -m "<message>" |
push(remote, branch) |
git push <remote> <branch> — returns false on failure, never throws |
When constructed with dryRun: true, every write operation prints what it would do and returns immediately — reads (diff, status) still execute so the message can be generated.
Parses the raw diff text with no external dependencies:
-
Parse — regex scans for
diff --githeaders to extract file paths. Chunk context determines status (new file mode→ added,deleted file mode→ deleted,rename to→ renamed, else modified). -
Detect type — ordered rule table:
- All test files →
test - All markdown/docs →
docs - All CSS/SCSS →
style - All config/lock files →
chore - Any new
.ts/.jsfile →feat - Diff text contains fix/bug/error/patch →
fix - Fallback →
chore
- All test files →
-
Detect scope — iterates the directory parts of each changed file path to find the longest common prefix (e.g.
src/auth/login.ts+src/auth/register.ts→ scopesrc/auth). -
Build description — groups files by action verb (
add,update,remove,rename). Lists up to 3 file basenames per verb, then appends+N more. -
Assemble + truncate — joins into
type(scope): description, truncates to 72 chars.
The entire function is synchronous and pure — given the same diff it always produces the same message.
Owns the lifecycle. On startup it loads config, constructs GitService, and starts FileWatcher. When the watcher fires onBatchReady:
- Conflict guard — skips if
git statusshows unresolved conflicts. - Concurrency guard —
isProcessingflag prevents a second batch running while the first is still in progress. - Stage → diff — calls
git.stageAll()thengit.getStagedDiff(). If diff is empty (nothing changed from Git's perspective), exits silently. - Generate — calls
generateCommitMessage(diff)synchronously. - Commit — calls
git.commit(message). - Push — if
autoPushis true, callsgit.push(). Logs a warning on failure but does not throw. - Error handling — entire flow is wrapped in try/catch/finally;
isProcessingis always reset infinally.
Singleton (export const logger) used by all modules. Each log call:
- Prints to the console (
console.log/warn/error) with a human-readable prefix. - Appends a JSON line to
.auto-commit.log. - Before appending, checks the file size — if ≥ 5 MB it renames the file to
.auto-commit.log.1(overwriting any older backup) and starts a fresh file.
Logging failures are silently swallowed so a disk-full situation never crashes the watcher.
Parses --dry-run and --help flags from process.argv. Loads .env via dotenv. Constructs AutoCommitService and calls start(). Registers SIGINT/SIGTERM handlers that call service.stop() for a clean shutdown (closes the chokidar watcher, clears the debounce timer).
1. User saves a file
│
▼
2. chokidar emits 'change' event
│
▼
3. FileWatcher adds path to changeBuffer, resets debounce timer
│
▼ (timer expires after debounceSeconds with no new events)
│
4. onBatchReady(paths[]) called
│
▼
5. AutoCommitService.processBatch()
├─ hasUnresolvedConflicts? → skip
├─ isProcessing? → skip
├─ git add -A
├─ git diff --cached → empty? → skip
├─ generateCommitMessage(diff)
├─ git commit -m "<message>"
└─ git push origin main → fail? → log warning, continue
│
▼
6. Logger writes JSON line to .auto-commit.log
| Package | Role |
|---|---|
chokidar |
Cross-platform file system watching |
simple-git |
Promise-based wrapper around the git CLI |
zod |
Runtime config schema validation with TypeScript inference |
dotenv |
Loads .env into process.env at startup |
typescript |
Type safety and compilation |
tsx |
TypeScript execution for dev mode (no build step) |
pm2 |
Process manager — keeps the service alive, handles restarts |
No AI SDK. No network calls for commit message generation. No API keys required.