README
ยถ
omc
oh-my-commit โ one command, no flags. Signed, AI-described commits your team can actually trust.
omc (spoken: "oh-my-commit") is a plain, stupid-simple git auto-commit,
sign, tag, and push utility written in pure Go. It takes zero command line
arguments and has zero runtime dependencies โ not even git needs to
be installed. Every behavior is an environment variable. Inside any git
working tree, one 3-letter command does the equivalent of:
#!/bin/sh
git add -A
git commit -S -m <generated detailed commit message>
git tag -s <old_semver+1>
[opt: git push]
[opt: git push --tags]
It supports agent/human role separation for signed commits, including LLM-generated commit messages and final review/push by humans.
Features ยท Quick Start ยท Demo ยท Why ยท Agentic Workflows ยท How it works
โจ Features
๐ซ Zero arguments, zero TTYEvery option is an environment variable, so it composes in scripts, aliases, pre-commit hooks, and โ critically โ inside an unattended agent that has no keyboard and must not be asked for one. |
๐ข Native git in GoRepository discovery, |
๐ SSH-signed commitsPoint Smartcard (FIDO2) keys โ |
๐ค AI commit messagesWith |
๐ค Flexible identity
|
๐ก๏ธ Always does the right, minimal thingNo repo โ clear error. No Ollama โ try the OpenAI-compatible fallback โ |
๐ท๏ธ Auto semver taggingEvery successful commit is immediately tagged with the next patch version ( |
โ๏ธ Message & tag overridesSet |
๐ Idempotent & predictableNo flags to remember, no prompts to answer. No Tags that collide. Just configure (when needed env) then 3 letter Autopilot everywhere. |
๐ Optional pushSet Like signing, the push key accepts a FIDO2 security-key handle ( |
๐ Quick Start
Run from anywhere inside a repo. That's it.
go run paepcke.de/omc/cmd/omc@latest
Install
go install paepcke.de/omc/cmd/omc@latest
Or build from source:
git clone https://github.com/paepckehh/omc
cd omc
make build
sudo install -m0755 omc /usr/local/bin/
Run
export OLLAMA_DESC_URL=http://127.0.0.1:11434 # optional: AI messages
export OMC_SIGN_KEY_PATH=~/.ssh/id_ed25519 # optional: SSH signing
omc
A one-shot signed commit with an explicit subject and tag:
OMC_SIGN_KEY_PATH=~/.ssh/id_ed25519 \
OMC_SUBJECT="fix: harden login against timing attacks" \
OMC_TAG="v1.4.0" \
omc
Requirements
- Go 1.26+ to build (the binary itself is a single static executable).
- Optional: a running Ollama server for AI-generated commit messages.
- Optional: an SSH private key to sign commits.
๐ฌ Demo
When stderr is a terminal, omc renders a structured, timestamped TUI
built on lipgloss: every line is a structured log record of the form
<HH:MM:SS> <LEVEL> omc [<step>] <message> [key=value ...], with
animated spinners per pipeline step, color-coded levels (OK / INFO / WARN
/ FAIL), and an animated touch countdown when a FIDO2 security key is
used. Related steps are gathered into nested log groups โ ๐ preparing repository, ๐ฌ message generation, ๐ signing key, and ๐ฆ committing & publishing (the latter with ๐ commit / ๐ท๏ธ tag / ๐ push sub-groups) โ each group header is itself a timestamped line, and
each record is prefixed with a tree connector (โโ first, โโ middle,
โโ last, one per open level, like git log --graph) so the whole run
reads as one tree.
The blocks below are real captured output (no Ollama running, so the
message falls back to update). Diagnostics and progress go to stderr;
the final commit and tag results go to stdout.
1. Software signing key
$ export OMC_SIGN_KEY_PATH=~/.ssh/id_ed25519
$ echo 'func main() { fmt.Println("hi") }' >> main.go
$ omc
14:18:31 โน๏ธ INFO omc omc v0.1.38
14:18:31 โน๏ธ INFO omc config detected environment count=1 OMC_SIGN_KEY_PATH=~/.ssh/id_ed25519
14:18:31 โน๏ธ INFO omc config verified config count=1 sign_key=valid=true
14:18:31 ๐ preparing repository
14:18:31 โโ โ
OK omc ๐ open done
14:18:31 โโ โน๏ธ INFO omc repo detected repository context dir=/home/ada/demo latest_tag=(none) origin_url=git@github.com:ada/demo.git
14:18:31 โโ โ
OK omc ๐ฅ stage done
14:18:31 โโ โ
OK omc ๐ diff done
14:18:31 โน๏ธ INFO omc ๐ diff changed files count=2
โบ .gitignore
โบ main.go
โ commit message
โญโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฎ
โ ๐ฌ update โ
โฐโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฏ
14:18:31 ๐ signing key
14:18:31 โโ โ
OK omc ๐ load key done
14:18:31 โโ โน๏ธ INFO omc signing with ~/.ssh/id_ed25519 (ssh-ed25519)
14:18:31 ๐ฆ committing & publishing
14:18:31 โโ ๐ commit
14:18:31 โโ โโ โน๏ธ INFO omc โ๏ธ sign signing commit with ssh key key=~/.ssh/id_ed25519
14:18:31 โโ โโ โน๏ธ INFO omc ๐ commit committing as Ada Lovelace <ada@example.com> (signed)
14:18:31 โโ โโ โ
OK omc ๐ commit committed hash=3f8bc54 signed=true
14:18:31 โโ ๐ท๏ธ tag
14:18:31 โโ โโ โ
OK omc ๐ท๏ธ tag done
14:18:31 โโ โโ โ
OK omc ๐ท๏ธ tag tagged tag=v0.0.1 hash=3f8bc54 signed=true
2. Smartcard (FIDO2) signing key โ touch countdown
With OMC_SIGN_KEY_PATH pointing at an id_ed25519_sk handle, omc signs
through the ssh-agent. On a TTY a live countdown animates while the signature
waits for the device touch, and the moment the operation returns the
countdown is replaced by a timestamped "touch confirmed, thank you" line
inside the tree plus a structured record naming what can now proceed:
$ ssh-add ~/.ssh/id_ed25519_sk
$ export OMC_SIGN_KEY_PATH=~/.ssh/id_ed25519_sk
$ omc
...
14:22:09 ๐ signing key
14:22:09 โโ โ ๏ธ WARN omc warning: ssh key ~/.ssh/id_ed25519_sk is a smartcard security key; signing via the ssh-agent
14:22:09 โโ โน๏ธ INFO omc โ๏ธ sign signing commit with ssh-agent security key key=~/.ssh/id_ed25519_sk mode=smartcard algo=sk-ssh-ed25519@openssh.com
14:22:09 โโ โน๏ธ INFO omc ๐ touch security key detected: touch your smartcard/yubikey when it blinks to authorise the commit signing key=~/.ssh/id_ed25519_sk mode=smartcard action=the commit signing
14:22:09 ๐ฆ committing & publishing
14:22:09 โโ ๐ commit
14:22:09 โโ โโ โน๏ธ INFO omc ๐ touch security key detected: touch your smartcard/yubikey when it blinks to authorise the commit signing key=~/.ssh/id_ed25519_sk mode=smartcard action=the commit signing
๐ TOUCH YOUR SECURITY KEY โฐโฐโฑโฑโฑโฑโฑโฑโฑโฑโฑโฑโฑโฑโฑโฑโฑ โฑ 0:21
14:22:14 โโ โโ ๐ touch confirmed, thank you!
14:22:14 โโ โโ โ
OK omc ๐ touch touch confirmed, thank you now=proceeding with the commit signing key=~/.ssh/id_ed25519_sk
14:22:14 โโ โโ โ
OK omc ๐ commit committed hash=5c91e2a signed=true
14:22:14 โโ ๐ท๏ธ tag
14:22:14 โโ โโ โน๏ธ INFO omc ๐ touch security key detected: touch your smartcard/yubikey when it blinks to authorise the tag signing key=~/.ssh/id_ed25519_sk mode=smartcard action=the tag signing
๐ TOUCH YOUR SECURITY KEY โฐโฑโฑโฑโฑโฑโฑโฑโฑโฑโฑโฑโฑโฑโฑโฑโฑ โฑ 0:29
14:22:18 โโ โโ ๐ touch confirmed, thank you!
14:22:18 โโ โโ โ
OK omc ๐ touch touch confirmed, thank you now=proceeding with the tag signing key=~/.ssh/id_ed25519_sk
14:22:18 โโ โโ โ
OK omc ๐ท๏ธ tag tagged tag=v0.0.2 hash=5c91e2a signed=true
When the agent has no matching identity, omc degrades โ never blocks:
14:23:55 โ ๏ธ WARN omc warning: ssh key ~/.ssh/id_ed25519_sk is a smartcard security key, but no ssh-agent identity matches
(connect to ssh-agent for security key ~/.ssh/id_ed25519_sk (is ssh-agent running?): dial unix: missing address); committing unsigned
14:23:55 โ
OK omc โ๏ธ sign committing unsigned signed=false
3. Subject / message / tag override
Skip the LLM entirely and write the commit text + tag name yourself:
$ OMC_SUBJECT="feat: add greeting to main" \
OMC_MESSAGE='Adds a fmt.Println("hi") entrypoint so the binary does something on run.' \
OMC_TAG="v1.0.0" \
OMC_SIGN_KEY_PATH=~/.ssh/id_ed25519 \
omc
...
14:24:10 โน๏ธ INFO omc config detected environment count=4 OMC_SIGN_KEY_PATH=~/.ssh/id_ed25519 OMC_SUBJECT=feat: add greeting to main OMC_MESSAGE=Adds a fmt.Println("hi") entrypoint so the binary does something on run. OMC_TAG=v1.0.0
14:24:10 โน๏ธ INFO omc config verified config count=2 sign_key=valid=true tag_override=valid=true
...
14:24:10 โน๏ธ INFO omc message override active (OMC_SUBJECT/OMC_MESSAGE)
14:24:10 โน๏ธ INFO omc โ๏ธ sign signing commit with ssh key key=~/.ssh/id_ed25519
14:24:10 โน๏ธ INFO omc ๐ commit committing as Ada Lovelace <ada@example.com> (signed)
14:24:11 โ
OK omc ๐ commit committed hash=3691ad8 signed=true
14:24:11 โน๏ธ INFO omc ๐ท๏ธ tag tagging v1.0.0
14:24:11 โ
OK omc ๐ท๏ธ tag tagged tag=v1.0.0 hash=3691ad8 signed=true
$ git show --stat --oneline HEAD
3691ad8 feat: add greeting to main
main.go | 4 ++++
1 file changed, 4 insertions(+)
4. Optional push
With OMC_PUSH_KEY_PATH set, the new commit and tag are pushed to the
default remote after tagging. A security-key push path shows the same touch
countdown during SSH authentication:
$ export OMC_PUSH_KEY_PATH=~/.ssh/id_ed25519
$ omc
...
14:25:02 โ
OK omc ๐ท๏ธ tag tagged tag=v1.0.1 hash=3691ad8 signed=true
14:25:02 ๐ push
14:25:02 โโ โน๏ธ INFO omc ๐ push pushing commit and tags to remote
14:25:03 โโ โ
OK omc ๐ push pushed remote=origin branch=main tags=true
Piped or non-interactive output (CI logs, captured tests) automatically falls back to the same structured, greppable line format:
$ omc 2>&1 | cat
12:04:07 โน๏ธ INFO omc omc v0.1.38
12:04:07 โน๏ธ INFO omc config detected environment count=2 OMC_SIGN_KEY_PATH=~/.ssh/id_ed25519 OLLAMA_DESC_URL=http://127.0.0.1:11434
12:04:07 โน๏ธ INFO omc config verified config count=2 sign_key=valid=true ollama=configured=true
12:04:07 โน๏ธ INFO omc ๐ open detecting repository
12:04:07 โ
OK omc ๐ open done
12:04:07 โน๏ธ INFO omc repo detected repository context dir=/home/ada/demo latest_tag=v0.3.8 origin_url=git@github.com:ada/demo.git
12:04:07 โน๏ธ INFO omc ๐ฅ stage staging all changes
12:04:07 โ
OK omc ๐ฅ stage done
12:04:07 โน๏ธ INFO omc ๐ diff reading staged diff
12:04:07 โ
OK omc ๐ diff done
12:04:07 โน๏ธ INFO omc ๐ diff changed files count=2
- internal/ollama/ollama.go
- cmd/omc/main.go
12:04:07 โน๏ธ INFO omc ๐ค ollama probing local ollama at http://127.0.0.1:11434
12:04:07 โ
OK omc ๐ค ollama done
12:04:08 โน๏ธ INFO omc ๐ค ollama generating commit message
12:04:09 โน๏ธ INFO omc ๐ค ollama condensing to TL;DR
12:04:09 โน๏ธ INFO omc ๐ฌ msg subject: sign commit payloads with git's SSH signature format
12:04:09 โน๏ธ INFO omc ๐ฌ msg body:
- Adds an armored BEGIN SSH SIGNATURE header to the commit object...
12:04:09 โน๏ธ INFO omc ๐ load key loading ssh signing key
12:04:09 โ
OK omc ๐ load key done
12:04:09 โน๏ธ INFO omc signing with ~/.ssh/id_ed25519 (ssh-ed25519)
12:04:09 โน๏ธ INFO omc โ๏ธ sign signing commit with ssh key key=~/.ssh/id_ed25519
12:04:09 โน๏ธ INFO omc ๐ commit committing as Ada Lovelace <ada@example.com> (signed)
12:04:10 โ
OK omc ๐ commit committed hash=9d3f2ab signed=true
12:04:10 โน๏ธ INFO omc ๐ท๏ธ tag bumping semver patch
12:04:10 โ
OK omc ๐ท๏ธ tag tagged tag=v0.3.9 hash=9d3f2ab signed=true
Every field is a separate, greppable token โ tag=v0.3.9, hash=9d3f2ab,
signed=true โ so downstream pipelines and log aggregators can parse them
without ambiguous whitespace. The commit and tag results go to stdout; all
diagnostics and progress go to stderr. When a FIDO2 security key is used on a
non-TTY, the animated countdown is suppressed and only the structured touch
notice and touch confirmed records are emitted, so CI logs stay greppable.
No arguments were typed. No git binary was spawned. No prompts were answered.
Everything that matters came from the environment.
๐ค Why another commit tool?
Commit hygiene is a security control, not a style preference. Signed, meaningful commits are how you:
- Hold agents accountable โ every change is attributable to a key you control.
- Keep history readable โ "fix typo in login" becomes
fix: harden the login flow against timing attacks. - Guard the supply chain โ a break in the signing chain is a break in the
trust your
maindepends on.
The problem: your AI coding agent happily runs git commit, but you don't want
it holding the same keys you use to push to the remote. omc solves
exactly that โ details under Secure agentic workflows.
๐งญ Table of Contents
- Features
- Quick Start
- Demo
- Why another commit tool?
- Secure agentic workflows
- Usage
- How it works
- Environment-only design
- Development
- License
๐ก๏ธ Secure agentic workflows
This is the scenario omc was built for. You're running an AI coding agent
โ Cursor, Claude Code, Crush, a CI bot โ and you want it to commit without
giving it the power to publish.
omc makes separation of duties trivial.
1. Separate signing keys from push keys (rights separation)
Your agent's shell gets only a signing key:
export OMC_SIGN_KEY_PATH=~/.ssh/keys/agent-commit # can SIGN commits, cannot push
export OLLAMA_DESC_URL=http://127.0.0.1:11434 # lets it describe its own work
omc
The push to the remote uses a different credential โ your human
~/.ssh/id_ed25519, a deploy key, or a short-lived CI token โ that the agent
never sees.
|| Activity | Credential | Holder |
|| -------- | ---------- | ------ |
|| Author + sign a commit locally | agent-commit signing key | the agent |
|| Push to the remote | your personal SSH key / deploy key | you |
Even if the agent is compromised, its key can authenticate against main all
it wants โ it can't publish a single object. The signing key signs; only your
push key publishes.
2. Human review before anything touches the remote
Because the agent only ever produces a local signed commit, the state of the remote is still fully yours to decide:
# 1. Agent does the work and signs it locally:
OMC_SIGN_KEY_PATH=~/.ssh/keys/agent-commit omc
# 2. YOU review the actual signed object:
git log --show-signature -1
# 3. Satisfied? Then โ and only then โ you push with your own key:
git push
Nothing has left your machine until step 3. The agent can't sneak work past you; every change is gated on a human verifying the signature and the diff.
Opt-in automation: if you do want the agent to publish, hand it a dedicated, low-privilege push key via
OMC_PUSH_KEY_PATH. The push still happens only after the commit and tag are created, and a failed push never rolls back the local work โ so the worst case is a local commit you can review and push yourself.
3. Attestation for audit trails
Signed, described commits give you a cryptographically verifiable record of
"what the agent did, in the agent's own words, under the agent's own key." When
OLLAMA_DESC_URL is set, the commit body is the model's own explanation of the
change โ so the commit message is the change's justification, auditable
forever in history.
Real-world use cases
- Agentic refactors at scale โ let the agent layer up small, descriptive, signed commits for a whole refactor, then review the set before a single push.
- CI / cron automation โ scheduled dependency bumps
git add -Aand commit with their own signing key; humans stay in charge of the merge. - Paired human+agent โ the agent drafts and signs, you review with
git show --show-signature, then push under your identity. Clean provenance for who wrote it vs. who published it. - Local-first AI โ the LLM talks to Ollama over localhost; your code, diffs, and commit bodies never touch a third-party API.
๐ Usage
All behavior is opt-in via environment variables. Nothing else.
omc
| Variable | Effect |
|---|---|
OMC_SIGN_KEY_PATH |
Path to an SSH private key. When set and valid, the commit is SSH-signed. A FIDO2 security-key handle (id_ed25519_sk / id_ecdsa_sk) is supported too: omc signs via the ssh-agent and your smartcard. If set but unusable, warns and commits unsigned. |
OLLAMA_DESC_URL |
Base URL of a local Ollama REST API, e.g. http://127.0.0.1:11434. When set and reachable, generates the commit message from the staged diff. |
OLLAMA_DESC_MODEL |
Ollama model name (optional). Defaults to llama3.2. |
OPENAPI_URL |
Base URL of an OpenAI-compatible REST API (any endpoint speaking /chat/completions + /models), e.g. http://host:11434/v1. Used as a fallback when Ollama is not configured or unreachable. The same prompts and response handling apply. (OPENAPI_DESC_URL is accepted as a legacy alias.) |
OPENAPI_MODEL |
Model name for the OpenAI-compatible endpoint (optional). Defaults to gpt-3.5-turbo. (OPENAPI_DESC_MODEL is accepted as a legacy alias.) |
OPENAPI_TOKEN |
Bearer token sent as Authorization to the OpenAI-compatible endpoint (optional). Some local endpoints accept a placeholder. The value is never printed; only (set) is reported. (OPENAPI_DESC_TOKEN is accepted as a legacy alias.) |
OMC_NAME |
Commit author/committer name (optional). Falls back to git config, then OMC, Git Commiter. |
OMC_EMAIL |
Commit author/committer email (optional). Falls back to git config, then git@omc.local. |
OMC_SUBJECT |
Override the commit subject. When set, no LLM generation runs. See Message & tag overrides. |
OMC_MESSAGE |
Override the commit body. When set, no LLM generation runs. See Message & tag overrides. |
OMC_TAG |
Override the tag name. Used only when it is strict semver vMAJOR.MINOR.PATCH; otherwise the auto-bump runs. See Message & tag overrides. |
OMC_PUSH_KEY_PATH |
Path to an SSH private key. When set and readable, pushes the new commit and tags to the default remote after tagging (git push; git push --tags). A FIDO2 security-key handle is supported (auth via ssh-agent + smartcard). If set but unusable, or the push fails, warns and leaves the commit/tag local. |
Signing: only passphrase-less software keys are supported (there is no interactive prompt, by design).
ssh-keygen -t ed25519 -N "" -C agent@paepcke.de -f ~/.ssh/agentis your friend. Scheduling note for CI: protect that key with filesystem permissions and rotate it like any credential. For smartcard keys you need your ssh-agent running and the key loaded (ssh-add ~/.ssh/id_ed25519_sk); the device then enforces the touch/PIN prompt itself.
๐ค AI message flow โ how the commit body is generated
When OLLAMA_DESC_URL is set and the server answers /api/tags:
omcstages everything and builds the staged diff (whatgit diffwould show against HEAD, with rename detection).- The diff is sent to the model, asking for a detailed, explanatory commit message โ what changed and why.
- That message is sent back in a fresh request, asking for a one-line TL;DR (max 72 chars, imperative mood).
- The TL;DR becomes the subject; the full details follow below it.
When Ollama is not configured or unreachable, omc falls back to an
OpenAI-compatible endpoint (OPENAPI_URL): it probes /models
(with the OPENAPI_TOKEN bearer token when set) and, on success, runs the
same two-pass generation against /chat/completions. The prompts and the
response handling are identical to the Ollama path, so commit messages are
comparable across backends. Ollama is always preferred (local-first); the
OpenAI-compatible endpoint is only the fallback.
The final message format is:
<TL;DR subject, โค72 chars>
<full detailed description from the LLM>
- First line: the shortened TL;DR (LLM) or
update(fallback or no Ollama). - Blank line, then the full detail body.
- When signing, an SSH signature header is embedded in the commit object; the payload signed is the commit without that header (git-conformant).
- The body is optional: if the LLM returns only a subject, that is used.
If the server is unreachable or generation fails, omc logs a warning and
falls back to the default subject update โ it never blocks a commit on the
network.
โ๏ธ Message & tag overrides โ OMC_SUBJECT / OMC_MESSAGE / OMC_TAG
Sometimes you already know what the commit (or the tag) should say โ the LLM
should not run. omc reads three optional override variables from the
environment, and any of them skips the Ollama two-pass generation:
OMC_SUBJECT="feat: harden login against timing attacks" \
OMC_MESSAGE="Adds constant-time comparison for the token check, ..." \
OMC_TAG="v1.4.0" \
omc
Subject / message pairing rules
OMC_SUBJECT |
OMC_MESSAGE |
Subject used | Body used |
|---|---|---|---|
| set | set | OMC_SUBJECT |
OMC_MESSAGE |
| set | unset | OMC_SUBJECT |
OMC_SUBJECT |
| unset | set | first line of OMC_MESSAGE (โค72 chars) |
full OMC_MESSAGE |
| unset | unset | LLM TL;DR (or update) |
LLM detail |
Whitespace around the values is trimmed. When only OMC_MESSAGE is set,
its first non-empty line becomes the subject (mirroring the โค72-char TL;DR
contract the LLM path uses); the full message is still kept as the body.
Tag override
OMC_TAG names the tag explicitly instead of bumping the patch of the
latest semver tag. It is used only when it parses as strict semver
vMAJOR.MINOR.PATCH โ an optional leading v, three non-negative integer
segments without leading zeros, no pre-release/build suffix. Arbitrarily
large values in any segment are accepted, so jumps like v999.0.0 are fine.
- A bare
1.2.3is normalized tov1.2.3. - An invalid override (e.g.
v1.2,v1.2.3-rc.1,latest,v01.2.3) is not used:omclogs a warning and falls back to the normalLatestSemverTag+NextSemverTagauto-bump. The commit is never rolled back over a bad tag override.
๐ท๏ธ Auto semver tagging โ how the tag is created
Immediately after a successful commit, omc tags that commit with a semver tag:
- If
OMC_TAGis set and parses as strict semvervMAJOR.MINOR.PATCH, that name (with a leadingvadded for bare versions) is used verbatim. Otherwise the auto-bump path runs. - All existing
refs/tags/v*.*.*refs are scanned; the highest semver version is selected (pre-release suffixes like-rc.1are ignored for comparison). - The patch segment is bumped by one (
v1.2.3โv1.2.4). When no semver tag exists yet, the first tag isv0.0.1. - An annotated tag object is created on the new commit. The tag message is the commit's subject line.
- When
OMC_SIGN_KEY_PATHis set and the key is valid, the tag is SSH-signed with the same key used for the commit โ the armoredBEGIN SSH SIGNATUREblock is embedded in the tag object, byte-compatible withgit tag -s. When no key is configured, an unsigned annotated tag is created instead.
The tag step always runs after a commit. If the tag step fails (e.g. a tag of
that name already exists), omc logs a warning and exits 0 โ the commit
itself is not affected.
$ git tag -l 'v*'
v0.0.1
$ git tag -v v0.0.1 # verify the SSH signature
object 9d3f2ab...
type commit
tag v0.0.1
tagger Ada Lovelace <ada@example.com>
sign commit payloads with git's SSH signature format
tagger signature verified:
๐ Pushing โ OMC_PUSH_KEY_PATH
After the commit and the semver tag are created, omc can push them to the
repository's default remote โ the go-git equivalent of git push; git push --tags, with no external git binary.
OMC_PUSH_KEY_PATHmust be set and the key readable. If it is unset, no push happens. If it is set but unreadable/unparseable,omclogs a warning and skips the push.- The current branch is pushed first (
refs/heads/<branch>), then all local tags (+refs/tags/*), mirroringgit push --tags. - The key authenticates over SSH. For non-SSH remotes (https/file) the key is not applicable and go-git's default auth is used. When the key path is empty, go-git falls back to its default auth (SSH agent).
- Security-key (FIDO2) keys work too:
OMC_PUSH_KEY_PATH=~/.ssh/id_ed25519_skis detected (by the conventionalid_*_skname and/or the.pubadjacent file) and authentication is delegated to the ssh-agent, which forwards the challenge to your smartcard โ the same waygit pushwithIdentitiesOnly+ a security key works. If the agent is missing or does not hold the key, the push is skipped with a warning. NoErrAlreadyUpToDateis treated as success. Any other failure (no remote, non-fast-forward, network) logs a warning and exits 0 โ the commit and tag are never rolled back over a push problem.- The push also runs when there is nothing to commit or tag (clean working tree): a previous run may already have committed and tagged locally while its push was skipped or failed, so the pending tags are published now. Failures degrade exactly like the main push step.
$ export OMC_PUSH_KEY_PATH=~/.ssh/id_ed25519
$ omc
...
14:25:02 โ
OK omc ๐ท๏ธ tag tagged tag=v1.0.1 hash=3691ad8 signed=true
14:25:02 โน๏ธ INFO omc ๐ push pushing commit and tags to remote
14:25:03 โ
OK omc ๐ push done
14:25:03 โ
OK omc ๐ push pushed remote=origin branch=main tags=true
With a security-key push path the SSH handshake shows the touch countdown:
$ export OMC_PUSH_KEY_PATH=~/.ssh/id_ed25519_sk
$ omc
...
14:26:10 โ
OK omc ๐ท๏ธ tag tagged tag=v1.0.2 hash=5c91e2a signed=true
14:26:10 โน๏ธ INFO omc ๐ touch security key detected: touch your smartcard/yubikey when it blinks to authorise the push key=~/.ssh/id_ed25519_sk mode=smartcard action=the push
๐ TOUCH YOUR SECURITY KEY โฐโฑโฑโฑโฑโฑโฑโฑโฑโฑโฑโฑโฑโฑโฑโฑโฑ โฑ 0:27
๐ touch confirmed, thank you!
14:26:15 โ
OK omc ๐ touch touch confirmed, thank you now=proceeding with the push key=~/.ssh/id_ed25519_sk
14:26:15 โ
OK omc ๐ push done
14:26:15 โ
OK omc ๐ push pushed remote=origin branch=main tags=true
๐ค Git identity resolution order
internal/gitops.ResolveIdentity() resolves the commit identity in this order:
OMC_NAME/OMC_EMAIL(omc's own variables)GIT_AUTHOR_NAME/GIT_AUTHOR_EMAILthenGIT_COMMITTER_*(standard git variables)user.name/user.emailfrom the repository's git config (read via go-git, no external binary)- Defaults:
OMC, Git Commiter <git@omc.local>
Environment always wins over git config. Config files are only consulted for the identity fallback; nothing else depends on them.
โ๏ธ How it works
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ omc (pure Go, no git binary, no CLI args) โ
โ โ
โ 1. find enclosing repo (go-git PlainOpen, walk up) โ
โ 1b. report repo context (dir, remotes, latest tag) โ
โ 2. stage all (Worktree.AddWithOptions All) โ
โ 3. build staged diff (HEAD tree โ index tree) โ
โ 4. optional: LLM two-pass message generation โ
โ (Ollama, fallback to OpenAI-compatible endpoint) โ
โ 5. optional: SSH sign (hiddeco/sshsig, "git" ns, sha512) โ
โ 5b. optional: smartcard touch countdown (FIDO2 sk keys) โ
โ 6. create commit (object.Commit, advance HEAD) โ
โ 7. auto-tag (latest v*.*.* โ patch+1, signed) โ
โ 8. optional push (OMC_PUSH_KEY_PATH โ git push --tags)โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
The commit object is written exactly as git writes it: tree, parents,
author/committer, message, and โ when signing โ a gpgsig-style SSH signature
header covering the header-less payload. The result is a first-class signed
commit that git log --show-signature and git verify-commit accept. omc
builds its own trees from the index, byte-identical to what git would write, so
verification never hiccups. The tool itself no longer prints a git log-style
history block; it emits a single structured committed record and leaves
history inspection to the user.
๐๏ธ Where things live (project layout)
cmd/omc/ entry point (env โ pipeline โ output)
internal/config/ config.FromEnv() reads the env vars
internal/gitops/ PlainOpen, StageAll, StagedDiff, Commit,
SignedCommit, ResolveIdentity, Scout,
indexโtree writer, semver tag discovery,
CreateTag, SignedTag, PushToRemote
internal/sign/ sign.Load, sign.SecurityKeySigner,
signer.Sign โ armored SSH sig; FIDO2 detection
internal/ollama/ Client.Available, DescribeDetail, SummarizeTLDR
internal/openai/ OpenAI-compatible fallback (same Generator contract)
internal/llm/ shared prompts + Generator interface for all backends
internal/output/ UI: stdout = structured results, stderr = diagnostics;
animated scramble spinner, touch countdown, log groups
๐ Environment-only design
omc deliberately parses no command line parameters because it's built for
places a human isn't watching:
- Scriptable โ
OMC_SIGN_KEY_PATH=โฆ OLLAMA_DESC_URL=โฆ omcanywhere. - Hookable โ drop it in a
pre-commithook or a globalalias commit=omc. - Composable โ nothing to remember, nothing to prompt for, plugs straight into
the non-interactive
$PROMPT_COMMAND/ agent shells where flags are a liability. - Air-gapped by default โ the only network call is to your own Ollama (or an OpenAI-compatible endpoint you configure), so the tool ships no telemetry and phones no home.
๐ ๏ธ Development
make test # go test ./...
make build # go build -o omc ./cmd/omc
make vet # go vet ./...
๐งช Testing notes
go test ./...โ full suite (config, sign, ollama, gitops, binary e2e).-shortskips the binary e2e test that compiles the CLI.- Tests use
t.TempDir()+os.Chdir(dir); they create real git repos withgit.PlainInitand never shell out to git except the e2e binary test (which skips ifgitis unavailable). writeIndexTreebuilds tree objects from the index; it must produce trees byte-identical to git so thatgit log --show-signatureandgit verify-commitaccept signed commits.- Always run
go vet ./...after changes.
๐งญ Worktree / OS notes
- Repositories are opened with
PlainOpenWithOptions(DetectDotGit: true)from the current directory โ subdirectories work. - Not being in a repo is an error.
- If the key file is missing/invalid when configured, log a warning and commit
unsigned. If Ollama is unreachable or the LLM call fails, fall back to the
default subject
updateand continue.
๐ License
MIT. See LICENSE.
Built with go-git ยท lipgloss ยท hiddeco/sshsig ยท Ollama
Directories
ยถ
| Path | Synopsis |
|---|---|
|
cmd
|
|
|
omc
command
Command omc is a plain, stupid-simple git commit helper.
|
Command omc is a plain, stupid-simple git commit helper. |
|
internal
|
|
|
config
Package config reads omc's entire configuration from the environment.
|
Package config reads omc's entire configuration from the environment. |
|
gitops
Package gitops implements the git operations behind omc: repository detection, staging, diffs, committing and history output.
|
Package gitops implements the git operations behind omc: repository detection, staging, diffs, committing and history output. |
|
llm
Package llm defines the shared commit-message generation contract used by every LLM backend omc supports (Ollama, OpenAI-compatible endpoints).
|
Package llm defines the shared commit-message generation contract used by every LLM backend omc supports (Ollama, OpenAI-compatible endpoints). |
|
ollama
Package ollama implements the optional LLM step of omc.
|
Package ollama implements the optional LLM step of omc. |
|
openai
Package openai implements the optional LLM step of omc against any OpenAI-compatible REST API endpoint (anything that speaks the /chat/completions and /models routes).
|
Package openai implements the optional LLM step of omc against any OpenAI-compatible REST API endpoint (anything that speaks the /chat/completions and /models routes). |
|
output
Package output emoji: cool state + action glyphs used to decorate the structured log records.
|
Package output emoji: cool state + action glyphs used to decorate the structured log records. |
|
sign
Package sign implements SSH signing of git commit objects, the exact equivalent of "git commit -S" with an SSH key (git's ssh format).
|
Package sign implements SSH signing of git commit objects, the exact equivalent of "git commit -S" with an SSH key (git's ssh format). |
|
version
Package version holds the hardwired build version of omc.
|
Package version holds the hardwired build version of omc. |