One command. Zero flags. Every commit described, signed, semver-tagged and pushed.
git add -A β AI commit message β SSH signature β semver tag β optional push.
Pure Go, one static binary, no git process at runtime.
Quick start Β· 30-second demo Β· See it run Β· Configuration Β· Agent workflows Β· How it works
The hero banner is the whole run in one frame: $ omc expanded into its seven
steps, each mapped to the git command it replaces. GitHub picks the variant
matching your theme; the hand-built SVG stays sharp at any width.
$ omcNo subcommands, no flags, no interactive prompts. Every behavior is an environment variable, so the same binary works in your shell, in CI, and inside an AI coding agent with no keyboard attached.
$ omc
ββ repository discovered git add -A
ββ staged diff read edit history, not just file names
ββ commit message generated local Ollama, or an OpenAI-compatible API
ββ commit SSH-signed software key or FIDO2 smartcard
ββ upstream tags fetched collision-proof semver bump
ββ tag created and pushed annotated, signed, published with the commit
Three letters. No
git commit -am, no-S, no--tags, no flags to forget.alias commit=omcand move on.
go install paepcke.de/omc/cmd/omc@latestThen, from anywhere inside a git repository:
omcThat is already a working commit: staged, messaged, tagged v0.0.1, done. Now
turn on the good parts:
export OLLAMA_DESC_URL=http://127.0.0.1:11434 # local AI message
export OMC_SIGN_KEY_PATH=~/.ssh/id_ed25519 # SSH-signed commit + tag
export OMC_PUSH_KEY_PATH=~/.ssh/id_ed25519 # push after tagging
omcOr run it straight from the module cache, no install step:
go run paepcke.de/omc/cmd/omc@latestBuild from source:
git clone <this repo>
cd omc
make build
sudo install -m0755 omc /usr/local/bin/make build stamps the latest git tag into the binary with -ldflags. A plain
go build ./cmd/omc uses the hardwired version.
Most commit helpers wrap git and add flags. omc is the pipeline: it
opens the repository, stages the tree, reads the diff, asks a local model,
signs with your key, writes the objects, bumps the tag and pushes. One binary,
no shell-outs, nothing half-finished.
|
Signed, well-described commits are an audit trail. Every object is attributable to a key you control, and the message explains the change in the author's own words. |
An agent should be able to commit without being able to publish. Give it a signing key with no push rights. Review the signed object. Push it yourself with your own key. |
|
No flags, no config file, no daemon, no telemetry. One binary. Environment variables in, structured logs out. |
Broken key? Commit unsigned. Ollama down? Try OpenAI. OpenAI down? Use
|
A normal commit records what changed. An omc commit records what, why,
who signed it, and which release it belongs to:
commit 7ed5573
Author: you <you@example.com>
fix: harden login against timing attacks
Uses constant-time comparison for bearer tokens.
Generated locally, reviewed before push.
gpgsig -----BEGIN SSH SIGNATURE-----
U1NIU0lHAAAAAQAAADMAAAALc3NoLWVk...
-----END SSH SIGNATURE-----
tag v0.1.55
Tagger: you <you@example.com>
fix: harden login against timing attacks
-----BEGIN SSH SIGNATURE-----
U1NIU0lHAAAAAQAAADMAAAALc3NoLWVk...
-----END SSH SIGNATURE-----
Verify any of it with stock git:
$ git verify-commit HEAD
$ git tag -v v0.1.55That is the demo. No dashboards, no service, no lock-in: your existing remote and your existing keys already understand all of it.
A real run, start to finish: a scratch repo, one edited file, an SSH key, and nothing else. This is the actual terminal output, wrapped only at the window edge. Signing, the commit hash, and the tag are all genuine.
More terminal captures below. Expand to view them.
πΈ Screenshots β full pipeline, FIDO2 touch, CI mode
A real YubiKey never blocks the run. Every smartcard wait gets a structured
record, an animated countdown, a desktop notification after a few seconds, and a
hard deadline (OMC_TOUCH_TIMEOUT, default 60s). A missed touch degrades to an
unsigned commit instead of hanging your terminal forever.
Piped logs use the same structured records without the animation:
<HH:MM:SS> <LEVEL> omc [<step>] <message> [key=value ...]
Field values containing whitespace are quoted, so grep 'signed=true',
grep 'tag=', and grep 'outcome=confirmed' always return clean tokens.
No remote, no config, no key required. Throwaway repo, one file, one command:
$ mkdir /tmp/omc-demo && cd /tmp/omc-demo && git init -q
$ echo "hello from omc" > hello.txt
$ omc
$ git log --oneline -1 && git tag --sort=-v:refname | head -1Result: a real commit with a generated message, already tagged v0.0.1.
Add OMC_SIGN_KEY_PATH and the same line produces a signature that
git verify-commit accepts. Add OMC_PUSH_KEY_PATH and the tag is already on
your remote before you think to ask.
That is the whole trick: the boring, error-prone part of shipping a change is now the default behavior of one command.
Everything is opt-in. With no variables at all, omc still creates an unsigned
commit tagged v0.0.1 with the subject update.
| Variable | What it does |
|---|---|
OMC_SIGN_KEY_PATH |
SSH private key used to sign the commit and tag. Software keys are loaded directly; FIDO2 handles (id_ed25519_sk, id_ecdsa_sk) sign through the ssh-agent and smartcard. Missing/broken β warn and commit unsigned. |
OLLAMA_DESC_URL |
Local Ollama base URL, e.g. http://127.0.0.1:11434. Enables AI commit messages. |
OLLAMA_DESC_MODEL |
Ollama model (default llama3.2). |
OPENAPI_URL |
OpenAI-compatible base URL, e.g. http://localhost:8080/v1. Fallback when Ollama is not configured, unreachable, or fails mid-generation. |
OPENAPI_MODEL |
Model for the OpenAI-compatible endpoint (default gpt-3.5-turbo). |
OPENAPI_TOKEN |
Bearer token for that endpoint. Never printed; logs show (set) only. |
OMC_NAME / OMC_EMAIL |
Commit identity. Falls back to GIT_AUTHOR_* / GIT_COMMITTER_*, then repo config, then OMC, Git Commiter <git@omc.local>. |
OMC_SUBJECT |
Exact commit subject. Skips AI generation. |
OMC_MESSAGE |
Exact commit body. Skips AI generation. |
OMC_TAG |
Explicit tag name. Used only when it is strict vMAJOR.MINOR.PATCH; otherwise auto-bump runs with a warning. |
OMC_PUSH_KEY_PATH |
SSH key (or FIDO2 handle) used to push the branch and tags after committing. Also authenticates the pre-tag upstream tag fetch. |
OMC_TOUCH_TIMEOUT |
Seconds to wait for a smartcard touch (default 60; 0 waits forever, matching OpenSSH). |
OMC_TOUCH_NOTIFY |
auto (detect a freedesktop notification daemon), on, or off. Detection only runs when a security-key path is configured. |
OPENAPI_DESC_URL, OPENAPI_DESC_MODEL, and OPENAPI_DESC_TOKEN are accepted
as legacy aliases for the OpenAI-compatible settings.
Warning
Keys must be passphrase-less. There is no interactive prompt by design:
ssh-keygen -t ed25519 -N "" -f ~/.ssh/agent. Protect the key with file
permissions and rotate it like any credential. Smartcard keys must be loaded
in a running ssh-agent (ssh-add ~/.ssh/id_ed25519_sk); the device enforces
the touch/PIN check itself.
When Ollama (or the OpenAI-compatible fallback) is reachable, the staged diff is sent in two passes:
- Detail β "explain what changed and why", using the per-file diff and the changed-file list.
- TL;DR β condense that detail into an imperative subject (β€72 chars).
The result is:
<TL;DR subject>
<detailed body>
Fallback order is deliberate and observable:
Ollama β OpenAI-compatible endpoint β update
A backend that probes fine but fails while generating is abandoned for the next one. If the detail pass succeeds but only the TL;DR pass fails, the detail is salvaged: its first line becomes the subject instead of throwing the whole generation away.
Large diffs are truncated per file section (200 lines triggers a 100-line cut, keeping the header and both ends), so one generated file cannot eat the model's context window.
π Local-first by default
Ollama runs on localhost; your code and diffs never leave the machine. An
OpenAI-compatible endpoint is only contacted when you configure one. omc has
no telemetry and phones no home.
Sometimes you already know the message. Any override skips AI generation.
OMC_SUBJECT="fix: harden login against timing attacks" \
OMC_MESSAGE="Uses constant-time comparison for bearer tokens." \
OMC_TAG="v1.4.0" \
omcOMC_SUBJECT |
OMC_MESSAGE |
Subject | Body |
|---|---|---|---|
| set | set | OMC_SUBJECT |
OMC_MESSAGE |
| set | unset | OMC_SUBJECT |
OMC_SUBJECT |
| unset | set | first non-empty line (β€72 chars) | full OMC_MESSAGE |
| unset | unset | AI TL;DR or update |
AI detail |
OMC_TAG is used verbatim only if it is strict semver: optional leading v,
three numeric segments, no leading zeros, no pre-release/build suffix. A bare
1.2.3 becomes v1.2.3. Anything else warns and falls back to the auto-bump.
After every commit, omc tags the new object:
- Fetch upstream tags first (
git fetch --tags, in-process go-git) so a tag pushed from another machine is visible before the name is generated. - Read the highest valid
vX.Y.Ztag. Pre-release suffixes like-rc.1use their base version; malformed tags (v1.2.3.4,v1.2.3junk) are ignored. - Bump the patch:
v1.4.2 β v1.4.3. No tag yet meansv0.0.1. - Create an annotated tag whose message is the commit subject.
- SSH-sign the tag with the same key as the commit, exactly like
git tag -s.
The fetch is best-effort and uses the remote's fetch URL. A repository with no remote still works; a failed fetch only warns and bumps from local state. If the tag step fails, the commit is never rolled back.
Set OMC_PUSH_KEY_PATH and omc pushes the current branch and all local tags
after tagging, the equivalent of git push && git push --tags:
$ omc
...
β
OK omc π·οΈ tag tagged tag=v1.4.3 hash=7ed5573 signed=true
β
OK omc π push pushed remote=origin branch=main tags=true- The branch is pushed first, then
+refs/tags/*(forced, likegit push --tags). - An already-up-to-date branch does not skip the tag pass: pending local tags from a previously failed push still get published.
- A clean working tree still runs the push, so those pending tags are not stranded.
- Any push failure warns and exits 0. The commit and tag stay local; nothing is rolled back.
- FIDO2 push keys authenticate through the ssh-agent. That SSH signature is a touch wait too, with the same prompt, notification, and timeout.
OMC_SIGN_KEY_PATH enables SSH signing in git's own format. The commit object
carries a gpgsig header with an armored BEGIN SSH SIGNATURE block; the tag
carries the same signature over its canonical payload.
$ git log --show-signature -1
$ git verify-commit HEAD
$ git tag -v v1.4.3Loaded directly in pure Go. No ssh-keygen, no gpg, no helper process.
id_ed25519_sk / id_ecdsa_sk handles are detected by name and by their .pub
file, then signed through the ssh-agent. The private half never leaves the
device; the authenticator enforces presence (and PIN).
A touch wait produces:
- a structured
touchrecord before the blocking signature; - an animated
π TOUCH YOUR SECURITY KEYcountdown and a terminal bell on a TTY; - a desktop notification after a few seconds via the freedesktop D-Bus
interface (GNOME, KDE, XFCE, β¦), no
notify-sendbinary; - a WARN record and a degrade path on timeout.
The same policy covers every smartcard operation: commit signing, tag signing, the upstream tag fetch, and the push.
Your agent can write code. It should leave a receipt, not a force-push. Give it a signing key with no push rights: it can author and sign, you review the object and publish with your own key.
# What the agent gets: sign only.
export OMC_SIGN_KEY_PATH=~/.ssh/keys/agent-commit
export OLLAMA_DESC_URL=http://127.0.0.1:11434
omc# What you keep: the push credential.
git log --show-signature -1 # review the signed object
git push # publish with your own key| Activity | Credential | Holder |
|---|---|---|
| Author and sign a commit locally | agent-commit signing key |
the agent |
| Publish to the remote | your SSH key / deploy key | you |
If you want the agent to publish, give it a dedicated low-privilege push key
via OMC_PUSH_KEY_PATH. The push happens only after the commit and tag exist,
and a failed push never loses local work.
More patterns:
- Agentic refactors β a stack of small, signed, described commits; review the set before one push.
- CI / cron β dependency bumps commit with a dedicated signing key; humans still control the merge.
- Audit trail β the model's change explanation becomes the commit body, stored forever next to the signature.
flowchart TD
A["omc β no args, env only"] --> B{"inside a repo?"}
B -- "no" --> X["FAIL: exit 1"]
B -- "yes" --> C["π₯ stage all (git add -A)"]
C --> D["π diff HEAD tree β index tree"]
D --> E{"nothing changed?"}
E -- "yes" --> PP["clean tree β push pending tags β exit 0"]
E -- "no" --> F{"OMC_SUBJECT / OMC_MESSAGE?"}
F -- "yes" --> G["use overrides verbatim"]
F -- "no" --> H["π¬ Ollama β OpenAI-compatible β update"]
G --> I{"OMC_SIGN_KEY_PATH?"}
H --> I
I -- "software key" --> J["βοΈ sign in pure Go"]
I -- "FIDO2 handle" --> K["π ssh-agent + smartcard touch"]
I -- "none / broken" --> L["warn β unsigned"]
J --> M["π commit"]
K --> M
L --> M
M --> N["π·οΈ fetch upstream tags"]
N --> O["bump semver patch (or OMC_TAG)"]
O --> P["annotated + SSH-signed tag"]
P --> Q{"OMC_PUSH_KEY_PATH?"}
Q -- "set" --> R["π push branch + tags"]
Q -- "unset / fails" --> Z["β
exit 0 β commit & tag stay local"]
R --> Z
| What went wrong | What omc does |
|---|---|
| Not inside a git repository | Fails with a clear error, exit 1 |
| Signing key missing, corrupt, passphrase-protected | Warns, commits unsigned |
| Security-key handle not loaded in ssh-agent | Warns, commits unsigned |
| Smartcard touch not received in time | Warns, commits unsigned; tag/pull/push degrade |
| Ollama unreachable | Probes the OpenAI-compatible endpoint |
| Ollama reachable but generation fails | Falls back to the OpenAI-compatible endpoint |
| Both AI backends unavailable or failing | Uses the subject update |
| No remote for the tag fetch | Bumps from local tag state |
| Tag already exists | Warns and exits 0; the commit is untouched |
| Push fails | Warns, exits 0; commit and tag remain local |
No subprocess, no git binary, no ssh-keygen, no notify-send. Repository
discovery, staging, diffs, commits, annotated tags, fetch, push, SSH
signing, D-Bus notifications β all libraries. The only external things omc
talks to are the services you configure: your Ollama/OpenAI endpoint and your
git remote.
make build # go build -o omc ./cmd/omc (stamps the git tag)
make test # go test ./...
make vet # go vet ./...
gofmt -s -w . # formatting gateThe module requires the Go version in go.mod. Tests create real git repos in
temporary directories; push/pull tests use an in-process git server, so no
network or binaries are needed. -short skips the binary e2e suite.
Contributions are welcome: small, well-tested, minimal diffs. Every commit is release-ready by default because the repo tags its own commits.
π§ͺ Test coverage notes
go test ./...covers config, signing, Ollama, OpenAI, gitops, output, and the compiled binary end to end.CGO_ENABLED=1 go test -race ./...exercises the spinner, touch prompt, and signing wrappers under the race detector.- Signed commits and tags are verified with the real
git verify-commit/git tag -vin the e2e tests. writeIndexTreebuilds git-byte-identical trees from the index so signature verification never hiccups.
ποΈ Where things live
cmd/omc/ pipeline: repo β stage β diff β message β sign β commit β tag β push
internal/config/ env-only configuration
internal/gitops/ open, stage, diff, commit, signed commit, tags, fetch, push
internal/sign/ SSH signing, FIDO2 detection, touch-bounded agent signing
internal/touch/ shared touch event vocabulary
internal/notify/ freedesktop notifications over D-Bus
internal/llm/ shared prompts + Generator contract
internal/ollama/ local Ollama backend
internal/openai/ OpenAI-compatible fallback backend
internal/output/ structured records, spinner, log groups, touch prompt
internal/version/ hardwired semver, overridable with -ldflags
docs/hero-*.svg theme-aware README hero banner (light/dark)
docs/established-*.svg theme-aware "established" banner (light/dark)
docs/branding/ icon, mark and the brand sheet (palette, geometry, rules)
docs/screenshots/ the terminal captures shown above
MIT. See LICENSE.