npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@synmux/claude-commit

v1.1.0

Published

Generate git commit messages with Claude, using your Claude Code subscription and/or Ollama.

Readme

claude-commit

Generate high-quality git commit messages with Claude - using your Claude Code subscription, not an API key.

claude-commit reads your staged diff, has a strong model summarize it, and a fast model turn that summary into a well-formed commit message. It handles diffs of any size (including ones too large to fit in a single context window), and supports Conventional Commits, gitmoji, first-line templates, custom instructions, and an interactive mode for choosing between several options.

$ cco -c
✔ Committed
feat(auth): add error handling and refresh token rotation to login

How it works

Want more details? See WALKTHROUGH.md.

staged diff ──ignore──▶ ──split──▶ [chunk, …] ──sonnet──▶ summaries ──sonnet──▶ commit message
  1. Summarize - the diff is split into chunks that fit the context window and each chunk is summarized by a strong model (sonnet, which carries a native 1M-token context). Diffs larger than 1M tokens simply produce more chunks. Paths listed in ignore are dropped first and never read at all; changes under configured low-priority paths are summarized separately, so churn cannot crowd out the code.
  2. Write - the summaries are handed to the same model (sonnet) to write the final commit message according to your formatting rules. The message is the whole point of the tool, and its input is tiny, so a strong model here costs almost nothing extra.

Either stage can run on a local Ollama model instead; by default both go through the Claude Agent SDK.

For less model work at the cost of less useful messages, enable filenamesOnly to skip summarisation entirely.

Install

Requires Node.js 22.18 or later (24 LTS recommended).

npm install -g @synmux/claude-commit   # `cco` and `claude-commit` on your PATH

From a checkout, pnpm is the package manager (the packageManager field pins the version, so corepack enable is enough):

pnpm install
pnpm link --global  # makes `cco` and `claude-commit` available on your PATH

Or run it directly without linking:

node bin/cco.js --help

Authentication

claude-commit uses the Claude Agent SDK and, by default, always authenticates with your Claude Code subscription session (run claude login once). Usage is bundled with your Claude Code usage - no separate API bill.

To protect you from surprise pay-as-you-go charges, API credentials in your environment (ANTHROPIC_API_KEY / ANTHROPIC_AUTH_TOKEN) are ignored by default: they are stripped from the environment passed to the model subprocess, and a one-line notice is printed to stderr. To bill an API key instead (pay-as-you-go), opt in explicitly in your configuration:

{ "allowApiKey": true }

Ollama models sit outside all of this: they run on a server you control, over plain HTTP, with no credential at all. See Ollama models.

Usage

cco [options]

By default cco summarizes your staged changes, generates a message, shows it, and asks for confirmation before committing. Pass -y to skip the prompt, or --dry-run to print the message without committing.

Options

| Flag | Description | | ---------------------------------------- | ------------------------------------------------------------------------------------------ | | -i, --interactive / --no-interactive | Choose between several options in an interactive picker, or skip it when enabled in config | | -n, --count <n> | Number of options to generate in interactive mode (default 3) | | -a, --all | Stage all changes (git add -A) before committing | | -c, --conventional | Format as a Conventional Commit | | -g, --gitmoji | Prefix the subject with a gitmoji | | -m, --multiline / --no-multiline | Write a multi-line commit (subject + body), or force a single line | | -t, --template <tpl> | Template for the first line, e.g. "[PROJ-1] {message}" | | -p, --prompt <text> | Extra instructions appended to the prompt | | -f, --filenames-only | Skip summarisation and send only filenames to the final model | | --model-summary <model> | Model used to summarize the diff (default sonnet) | | --model-final <model> | Model used to write the message (default sonnet) | | --skip-armored | Omit armored/encoded lines (age/gpg armor, base64 blobs) from the summarized diff | | --no-low-priority-paths | Ignore lowPriorityPaths for this run, so every change weighs the same | | --no-ignore | Disregard ignore for this run, so every staged change is read | | --ollama-host <url> | Base URL of the Ollama server for ollama: models | | --ollama-context <tokens> | Context window requested from Ollama models | | -d, --dry-run | Print the message to stdout without committing | | -y, --yes | Commit without asking for confirmation | | --no-spinner | Disable the progress spinner | | --config <path> | Path to a config file | | -v, --verbose | Print summaries, cost and debug output |

Examples

cco                      # generate, confirm, and commit staged changes
cco -a -c                # stage everything and write a Conventional Commit
cco -c -g -m             # conventional + gitmoji + a body
cco -i -n 5              # pick from 5 options interactively
cco -f --dry-run         # generate from filenames only, without committing
cco --dry-run | cat      # print a message without committing (no picker, pipe-safe)
git commit -F <(cco -d)  # use the message with your own git invocation

In a pipe (no TTY) there is no spinner and no confirmation prompt - cco just generates and commits (or prints, with --dry-run).

Filenames-only mode

Set "filenamesOnly": true in any config layer, or pass -f / --filenames-only, to send only the list of filenames touched by staged changes to models.final. The default is false.

{ "filenamesOnly": true }

The summariser is skipped entirely: no diff chunks, summary calls, or summary-model preload. The final model receives no file contents or diff hunks, so expect broader, less useful messages. It is instructed to describe the affected areas without inventing specific edits or reasons for them. Normal formatting, custom instructions and interactive options still apply.

ignore still removes matching file sections, and lowPriorityPaths still groups and weights the remaining filenames. Renames and copies include both paths; additions, deletions, binary files and mode changes are included. If every staged file is ignored, generation still stops with an error.

models.summary, maxChunkTokens, charsPerToken and skipArmored have no effect in this mode. --verbose reports that the summariser was skipped; library results have an empty summaries array and chunkCount: 0.

Interactive mode

cco -i opens a picker (built on Clack) listing several candidate messages, each with its subject and a one-line body preview. The options are generated with a higher temperature (interactiveTemperature) for more variety. Use the arrow keys (or j/k) to move between options, Enter to commit the highlighted option, e to edit it in your $EDITOR first, and q/Esc/Ctrl-C to cancel. The picker draws on stderr, so stdout stays clean.

To make interactive mode the default without typing -i every time, set "interactive": true in your config (see below); opt out of a single run with --no-interactive. When there is no interactive terminal - in a pipe, a CI job, or with --dry-run - cco ignores the setting and falls back to the non-interactive flow rather than failing.

Configuration

Configuration is layered, from lowest to highest precedence:

  1. Built-in defaults.
  2. A global user config at ~/.config/claude-commit/config.json (or $XDG_CONFIG_HOME/claude-commit/config.json) - your personal defaults across every project. The .claude-commit.json / .claude-commitrc.json / .claude-commitrc names are also accepted in that directory.
  3. A claude-commit key in the repo's package.json.
  4. The nearest .claude-commit.json / .claude-commitrc.json / .claude-commitrc, searched from the current directory up to the repo root.
  5. CLI flags.

So a global config sets your personal defaults and any project further down the tree can override them. Note that the config.json name is recognised only in the global directory; inside a project, use one of the dotted filenames. The same keys are valid at every level:

{
  "conventionalCommits": true,
  "gitmoji": true,
  "multiline": true,
  "template": null,
  "customPrompt": "Reference the ticket id from the branch name when present.",
  "interactive": true,
  "interactiveCount": 3,
  "interactiveTemperature": 1,
  "spinner": "material",
  "models": {
    "summary": "sonnet",
    "final": "sonnet"
  },
  "maxChunkTokens": 600000,
  "charsPerToken": 3.5,
  "filenamesOnly": false,
  "skipArmored": false,
  "lowPriorityPaths": [],
  "ignore": [],
  "ollama": {
    "host": "http://localhost:11434",
    "context": "auto",
    "keepAlive": null
  },
  "allowApiKey": false
}

spinner chooses the progress animation: any name from the cli-spinners set bundled with ora ("dots", "moon", "pong", "material", ...). Unknown names are ignored and the default material is used. --no-spinner disables the animated spinner, but final status lines still print.

The material spinner is chosen because it's fucking cool. Fight me.

maxChunkTokens is a cap, not a promise: at run time it is clamped to the summary model's context window minus a fixed reserve (1M-window models such as current Sonnet/Opus keep the full budget; Haiku, older pinned model ids, and unrecognised models are floored at 200k), so a single chunk can never overflow the model.

Chunk sizes come from a content-classified token estimate, not a flat charsPerToken ratio: armored or encoded lines (age/gpg armor, base64 blobs, git binary patches) tokenize at roughly one token per character on current Claude models, so they are budgeted at that rate while ordinary text keeps the configured ratio. If the backend still rejects a chunk as too long, cco re-splits just that chunk with a halved budget and retries - the rejection is free, so the API is the final arbiter.

skipArmored (or the --skip-armored flag) goes further and replaces each run of armored lines with a one-line [cco: N armored/encoded lines omitted] marker before summarizing. Ciphertext is unreadable to the model anyway, so this is the recommended setting for encrypted-file repos - for example a chezmoi source directory with age encryption, where every chezmoi re-add re-encrypts nondeterministically and produces megabytes of churned armor. Drop a .claude-commit.json with { "skipArmored": true } in the repo root to enable it per-repo.

Low-priority paths

Some paths change a lot without meaning much - generated docs, lockfiles, vendored snapshots, build output. Left alone, a commit that touches twenty lines of code and regenerates two thousand lines of tooling gets a subject line about the tooling. lowPriorityPaths lists gitignore-style patterns for those paths:

{
  "lowPriorityPaths": [
    ".agents/skills/*-skilld",
    "pnpm-lock.yaml",
    "generated/**",
    "!generated/schema.ts"
  ]
}

Changes under matching paths are summarised separately and briefly, and the model is told that the subject line - and the commit type, scope and gitmoji where you use them - comes from the other changes, however small they are. The low-priority changes are mentioned in the subject only if they fit, and in the body (with multiline) only after the primary changes. When every changed file is low priority there is nothing for it to yield to, so the changes are described normally, exactly as if no patterns were configured.

Pattern rules follow .gitignore conventions, so trunk or gitignore lines can usually be copied in:

  • A pattern with a / in it is anchored at the repository root and matches a path or any directory above it - .agents/skills/*-skilld covers every file inside each matching directory.
  • A pattern without a / matches any path segment at any depth - pnpm-lock.yaml matches packages/app/pnpm-lock.yaml; *-skilld matches everything inside any *-skilld directory.
  • * matches dotfiles and does not cross /; ** does; {a,b} expands. A leading / or ./ anchors, a trailing / is ignored. The anchoring decision looks at the whole pattern, so a / inside a brace group anchors all of its alternatives - prefer one pattern per intent.
  • A leading ! negates, and the last matching pattern wins: ["docs/**", "!docs/adr/**"] deprioritises docs except the ADRs.
  • Patterns are always matched against repository-root-relative paths with / separators, whichever directory you run cco from. A backslash in a pattern is an escape (\[, \{, \! for the literal characters), so Windows-style dist\** matches nothing.
  • Patterns are not validated: a typo such as an unbalanced { is parsed rather than rejected and may match something unexpected, so check the --verbose match counts when you add one.

A rename into or out of a low-priority path counts as primary (both sides must match). The nearest config layer that sets the key wins outright - lists are never merged - so "lowPriorityPaths": [] in a project opts out of a global list, and a project that wants the global patterns plus its own must repeat them. --no-low-priority-paths switches the feature off for one run, which is handy when the churn is the story, or for comparing messages while tuning patterns.

This changes how changes are weighted in the message, not how much of the diff is read: low-priority content is still summarised in full, at the same cost. To skip content outright, see skipArmored. Under --verbose, cco reports how many files matched (low-priority paths: matched 3 of 41 files), which is the only way to tell a pattern that matched nothing from one that matched everything and was promoted.

Ignoring paths entirely

lowPriorityPaths still reads everything it deprioritises, and pays for it. Some content is worth neither the tokens nor the time: a vendored dependency tree, a generated API client, a data fixture that changes wholesale.

ignore takes the same gitignore-style patterns and removes those file sections from the diff before anything else looks at it - before the low-priority partition, before chunking, before any model call:

{ "ignore": ["vendor/**", "**/__snapshots__", "*.generated.ts"] }

The stages compose in the order their names suggest:

diff ─ ignore ─▶ ─ skipArmored ─▶ ─ lowPriorityPaths ─▶ chunks ─▶ summaries

Two things worth being clear about:

  • The files are still committed. ignore governs what the model reads, never what git stages. cco is writing a message, not choosing a changeset.
  • When it matches everything, cco stops with an error naming the directive, rather than inventing a message about changes you told it not to read. This is deliberately unlike lowPriorityPaths, which promotes its partition in the same situation - "this matters less" can degrade gracefully, "do not look at this" has nothing to degrade to. Pass --no-ignore for that one commit.

As with lowPriorityPaths, a section is dropped only when it names at least one path and all of them match, so a rename out of an ignored directory survives. --verbose reports the count (ignore: dropped 3 of 41 files before reading).

Ollama models

Any model can be run on a local (or self-hosted) Ollama server instead of Claude, by prefixing its name with ollama:. Everything after the prefix is the Ollama model name verbatim, tag included:

{
  "models": {
    "summary": "ollama:ornith-1.5:35b",
    "final": "sonnet"
  }
}

The two stages resolve independently, so that mixed setup is the interesting one: reading the diff is the bulk of the work and the most sensitive thing cco touches, so it runs locally and free, while the final message - one short, quality-sensitive call on a summary - still goes to Claude. The prefix works anywhere a model name does, including the flags:

cco --model-summary ollama:ornith-1.5:35b --dry-run -v

The server needs no credential. cco talks to Ollama's native /api/chat endpoint, not either of its OpenAI/Anthropic compatibility layers, because only the native API can set a context length.

Context length is the setting that matters

Ollama picks a context window for each model from available VRAM (4k / 32k / 256k tiers, capped at the model's trained maximum), and a prompt that exceeds it is truncated silently - HTTP 200, oldest content dropped, nothing on the response to say so. A summary written from half a diff is worse than no summary, so cco never lets that number stay implicit: it sends an explicit window on every request, sizes its diff chunks against the same number, and checks the token counts afterwards to catch a truncation that happened anyway (in which case it re-splits the chunk and retries, exactly as it does for a Claude context overflow).

Where the number comes from is ollama.context:

{
  "ollama": {
    "host": "http://localhost:11434",
    "context": "auto",
    "keepAlive": "10m"
  }
}
  • context - "auto" (the default) asks the server rather than guessing: the model is preloaded with no window set, so Ollama applies its own VRAM-based choice, and cco reads that choice back from /api/ps before sizing anything. That is the largest window Ollama believes this machine can actually run - 131072 for a Gemma model on a large Mac, 4096 for the same model on a small laptop - resolved once per model per run, and shown under --verbose. A number pins the window instead: lower it when memory is tight (usage scales with it, multiplied by OLLAMA_NUM_PARALLEL), or raise it past the tier if you know your hardware better than the server does. A smaller window is never a correctness problem - cco just splits the diff into more chunks.
  • host - defaults to $OLLAMA_HOST, then http://localhost:11434. A bare box.local:11434 gains an http://, matching Ollama's own convention.
  • keepAlive - how long the server keeps the model loaded after a request: a duration string ("10m"), seconds as a number, 0 to unload immediately, or negative to pin it. null leaves the server's own default. Pinning is worth it if you commit often; a 35b model takes a while to load.

What differs from a Claude model

  • Cost is reported as zero, because local inference is not billed. In a mixed run, --verbose's total is exactly the Claude half.
  • Structured output is requested through Ollama's format field. A model or server that cannot honour it (Ollama Cloud does not support it at all) falls back to plain-text parsing automatically.
  • A missing model is an error, not a download. cco tells you to run ollama pull <model> rather than pulling tens of gigabytes on your behalf.
  • Reasoning is never requested, and any the model volunteers is discarded - models disagree about whether thinking can even be switched off, and asking is a good way to earn a 400.

Development

pnpm test           # run the test suite (vitest)
pnpm run typecheck  # tsc --noEmit
pnpm run build      # bundle bin/ and index.ts into dist/ (only needed to publish)

The sources run directly on Node's native type stripping, so there is no build step during development: node bin/cco.js picks up dist/ when it exists and falls back to the TypeScript entry otherwise.

What changed between versions is in CHANGELOG.md, and at greater length on the releases page.

Did you vibe this?

I distinguish vibe coding and AI-assisted development by where you live as the developer. If you live in the code, it's AI-assisted dev. If you just shout at a chat and hope for the best, that's vibe coding.

This was AI-assisted development.

Thanks for coming to my TED talk.