# How I log every Git commit to a plain text file

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

Author: Flavio Copes | Published: 2026-07-13 | Canonical: https://flaviocopes.com/log-git-commits-plain-text/

I recently wrote a [Summer of Code update](https://flaviocopes.com/the-summer-of-code-update-2/) 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:

```text
[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:

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

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

```python
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:

```python
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:

```text
~/.cursor/work-log.txt
```

I also keep a small state file:

```text
~/.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:

```bash
#!/bin/sh

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

Save it as:

```text
.git/hooks/post-commit
```

and make both scripts executable:

```bash
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:

```text
~/.cursor/hooks.json
```

```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:

```python
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.
