Skip to content
FLAVIO COPES
flaviocopes.com
2026

How I log every Git commit to a plain text file

By Flavio Copes

I use Git's native post-commit hook to keep one reverse-chronological work log, with Cursor installing it automatically in each project.

~~~

I recently wrote a Summer of Code update listing everything I had built over the previous three days.

To write it, I had to inspect the Git history of several projects and reconstruct what happened.

I wanted one plain text file that answers a simple question:

What did I build?

Every successful commit should appear in the file automatically. The newest work should be at the top, grouped by date and project.

Like this:

[2026-07-11]

  cookwithai.dev
    22:49 - Show latest blog posts on the homepage
    22:45 - Explain every free tool in plain language

  fstack
    22:42 - Add /interview skill
    22:38 - Add /roast skill

My first idea was to use a Cursor hook after git push.

That turned out to be the wrong place.

Why I changed from post-push to post-commit

Cursor has a postToolUse event that usually runs after an agent shell command.

I configured it to detect git push, tested it with a simulated event, and thought the job was done.

Then a Cursor agent made two real commits and pushed them. Nothing appeared in the log.

Cursor’s own Hooks output showed what happened. The hook loaded correctly and ran after other shell commands, but the real commit-and-push command never produced a postToolUse event.

Git also has no native client-side post-push hook.

It does have post-commit.

This is the right trigger. Git itself runs it after every successful commit. It does not matter if the commit came from Cursor, Codex, a terminal, or a Git interface.

The work log script

I keep the logger here:

~/.cursor/hooks/write-work-log-entry.py

The script asks Git for the current repository, branch, commit, and commit subject:

root = git('rev-parse', '--show-toplevel')
head = git('rev-parse', 'HEAD')
branch = git('branch', '--show-current')
subject = git('log', '-1', '--format=%s', 'HEAD')

It formats the entry:

now = datetime.now().astimezone()
entry = f'{now:%H:%M} - {subject}'

The logger parses the existing file, adds the entry below the project name, and moves that project to the top of the current date. When the day changes, it creates a new bracketed date section.

The result lives in:

~/.cursor/work-log.txt

I also keep a small state file:

~/.cursor/work-log-state.json

It stores the last recorded commit for each repository and branch. This prevents duplicate entries if the hook is called twice for the same commit.

The native Git hook

Inside a Git repository, hooks live under .git/hooks/.

The post-commit hook is a small shell script:

#!/bin/sh

"$HOME/.cursor/hooks/write-work-log-entry.py" || true

Save it as:

.git/hooks/post-commit

and make both scripts executable:

chmod +x ~/.cursor/hooks/write-work-log-entry.py
chmod +x .git/hooks/post-commit

The || true is deliberate. A logging problem should never affect a commit.

Once this hook exists, every commit in that repository enters the work log, no matter which application created it.

Installing the hook automatically from Cursor

A Git hook normally belongs to one repository. I work across many projects, so I did not want to install it by hand every time.

I added a Cursor beforeShellExecution hook. When a Cursor agent is about to run git commit, it installs the native post-commit hook in that repository first.

My user-level Cursor configuration lives at:

~/.cursor/hooks.json
{
  "version": 1,
  "hooks": {
    "beforeShellExecution": [
      {
        "command": "./hooks/ensure-work-log-post-commit.py",
        "matcher": "git\\s+commit",
        "timeout": 10,
        "failClosed": false
      }
    ]
  }
}

The installer runs before the commit. It finds the correct Git hooks directory:

hook_path = git(
  'rev-parse',
  '--git-path',
  'hooks/post-commit',
)

If no post-commit hook exists, it creates one.

If the project already has a post-commit hook, my installer moves it to a backup file and creates a wrapper that runs both hooks. Existing project automation keeps working.

After that first Cursor commit, the repository is set up permanently. Commits made later from a terminal or another app are logged too.

Why this is better

The old version depended on one agent application noticing one shell command.

The new version depends on Git noticing a commit.

That is the event I cared about all along.

I don’t log prompts, transcripts, diffs, or shell output. The commit subject is enough. It gives me a short record of completed work without turning the file into another database.

Now the next time I write an update, I can open one text file instead of searching through five repositories.

Tagged: AI · All topics
~~~

Related posts about ai: