Auto Commit
Source attribution: The git integration behavior described in this page is drawn from the official Aider documentation at aider.chat/docs/git.html. Implementation details are traced from
references/aider/aider/repo.pyandreferences/aider/aider/coders/base_coder.pyat commitb9050e1d5faf8096eae7a46a9ecc05a86231384b.
Feature Definition
Section titled “Feature Definition”When an AI agent edits files, those edits must land somewhere in the project’s version history. The naive approach — let the user commit manually after reviewing — sounds safe but breaks down in practice. Sessions generate dozens of incremental changes. If the user forgets to commit and then runs /undo, or the agent crashes mid-session, the edit history is gone. Worse, without discrete commits, the diff between “before AI assistance” and “after” is a single incomprehensible blob.
Auto-commit solves this by committing every accepted AI edit immediately after it’s applied. Each commit:
- Captures a precise snapshot of the AI’s contribution for future undo
- Generates a human-readable message describing what changed (not just “aider edit”)
- Attributes the change to the AI so reviewers know which commits were AI-authored
- Keeps user’s own uncommitted changes separate from AI changes
The hard parts are:
- Dirty repo handling: The user may have staged or unstaged changes before asking the agent anything. Blindly committing will either commit the user’s in-progress work under an AI message or produce a confusing mixed commit.
- Message quality: The commit message must be descriptive enough to be useful in
git logwithout being so long it obscures the diff. Generating this from a raw diff requires a capable model and a well-crafted prompt. - Attribution: Team projects need to know which commits are AI-generated. Naive approaches (just commit as the current user) make it impossible to audit AI involvement post-hoc.
- Undo safety: Auto-committed changes should be reversible with a single in-chat command. This requires tracking which commits the agent made in the current session.
Aider Implementation
Section titled “Aider Implementation”Reference: references/aider/aider/repo.py, references/aider/aider/coders/base_coder.py, references/aider/aider/commands.py | Commit: b9050e1d5faf8096eae7a46a9ecc05a86231384b
Auto-Commit Entry Point
Section titled “Auto-Commit Entry Point”After Aider applies edits to the filesystem, base_coder.py’s auto_commit() method fires immediately:
def auto_commit(self, edited, context=None): if not self.repo or not self.auto_commits or self.dry_run: return
res = self.repo.commit(fnames=edited, context=context, aider_edits=True, coder=self) if res: self.show_auto_commit_outcome(res)The aider_edits=True flag is the key. It tells repo.commit() that this is an AI-generated change, which triggers the attribution logic. The context parameter passes recent chat history to the commit message generator so the message can reflect what the user asked for, not just what bytes changed.
On success, show_auto_commit_outcome() records the commit hash in two places:
def show_auto_commit_outcome(self, res): commit_hash, commit_message = res self.last_aider_commit_hash = commit_hash self.aider_commit_hashes.add(commit_hash) # session-wide set for /undo tracking self.last_aider_commit_message = commit_messageaider_commit_hashes is a set that grows throughout the session. The /undo command checks this set before resetting HEAD — if the last commit’s hash is not in the set, Aider refuses to undo it (it’s a user commit, not an AI commit).
Commit Message Generation
Section titled “Commit Message Generation”The message is generated by repo.get_commit_message() in repo.py:
def get_commit_message(self, diffs, context, user_language=None):The system prompt (from aider/prompts.py) instructs a “weak” model to produce a Conventional Commits-style one-liner:
You are an expert software engineer that generates concise, one-line Git commit messagesbased on the provided diffs.Review the provided context and diffs which are about to be committed to a git repo.Generate a one-line commit message for those changes.The commit message should be structured as follows: <type>: <description>Use these for <type>: fix, feat, build, chore, ci, docs, style, refactor, perf, test
Ensure the commit message:- Starts with the appropriate prefix.- Is in the imperative mood (e.g., "add feature" not "added feature" or "adding feature").- Does not exceed 72 characters.
Reply only with the one-line commit message, without any additional text, explanations, or line breaks.Aider uses a “weak” model for this — typically gpt-3.5-turbo or a comparable cheap model — because the task is mechanical. There’s no need to burn Claude 3.5 Sonnet credits to generate a commit message. The implementation tries models in sequence until one succeeds within its token limit:
# From repo.py get_commit_message():for model in commit_message_models: try: commit_message = model.simple_send_with_retries(messages, max_tokens=25) if commit_message: return commit_message.strip().strip('"').strip() except Exception: continueThe strip('"') handles the common case where models wrap their one-liner in quotation marks despite the instruction not to.
The diff preparation deserves attention. For repos with prior commits, Aider uses git diff HEAD rather than git diff --cached. This means the diff includes unstaged changes too — which is intentional, since the files were just written by the edit applier and may not be staged yet:
def get_diffs(self, fnames=None): if not self.repo.head.is_valid(): # Empty repo: combine staged + unstaged diffs = self.repo.git.diff("--cached") diffs += self.repo.git.diff() else: diffs = self.repo.git.diff("HEAD", "--", *fnames)New files (not yet tracked by git) are prefixed with "Added {fname}\n" in the diff, so the commit message generator understands a file was created from scratch rather than modified.
Dirty Commit Handling
Section titled “Dirty Commit Handling”The user often has uncommitted work in their repo when they start an Aider session. Blindly committing after Aider’s edits would mix that uncommitted work into the AI commit. Aider’s solution is to commit dirty files before editing them:
def check_for_dirty_commit(self, path): if not self.repo or not self.dirty_commits: return if not self.repo.is_dirty(path): return self.need_commit_before_edits.add(path)This runs whenever Aider is about to edit a file. Files marked dirty accumulate in need_commit_before_edits. Before applying edits, dirty_commit() commits them:
def dirty_commit(self): if not self.need_commit_before_edits: return self.repo.commit(fnames=self.need_commit_before_edits, coder=self) # Note: aider_edits is NOT passed — this is a user-changes commitThe user’s in-progress work now lands in its own commit with an auto-generated message describing the user’s changes, and the AI’s edit will land in a separate subsequent commit. The resulting history is clean: user work, then AI work, clearly separated.
dirty_commits is forced to False when auto_commits=False — you can’t have pre-commit separation without post-commit as well:
if not auto_commits: dirty_commits = FalseAttribution System
Section titled “Attribution System”Aider writes its authorship into git metadata using environment variables that git respects during commit:
@contextlib.contextmanagerdef set_git_env(var_name, value, original_value): os.environ[var_name] = value try: yield finally: # restore original valueThe attribution logic depends on a set of constructor flags in GitRepo:
| Flag | Default | Effect |
|---|---|---|
attribute_author | True | Append (aider) to git author name |
attribute_committer | True | Append (aider) to git committer name |
attribute_commit_message_author | False | Prepend aider: to commit messages |
attribute_commit_message_committer | False | Append committer attribution to message |
attribute_co_authored_by | False | Add Co-authored-by: trailer to message |
For a typical session with defaults, the commit shows:
Author: Alice <alice@example.com> (aider)Commit: Alice <alice@example.com> (aider)
feat: add input validation to login formWhen attribute_co_authored_by=True, Aider instead adds a Co-authored-by trailer and does not modify the author/committer names (unless the corresponding flags are explicitly set):
Author: Alice <alice@example.com>Commit: Alice <alice@example.com>
feat: add input validation to login form
Co-authored-by: aider (claude-3-5-sonnet-20241022) <aider@aider.chat>The model name is extracted from coder.main_model.name, so the trailer identifies exactly which model generated the change.
In-Chat Git Commands
Section titled “In-Chat Git Commands”Aider exposes git operations as slash commands in the REPL, all handled in commands.py:
/commit — Commit staged user changes with an optional message:
def raw_cmd_commit(self, args=None): commit_message = args.strip() if args else None self.coder.repo.commit(message=commit_message, coder=self.coder) # aider_edits not passed → user commit, no (aider) attribution/undo — Revert the last Aider commit. Safety checks before resetting:
- Verify the HEAD commit hash is in
aider_commit_hashes(won’t undo user commits) - Verify the files have no uncommitted changes that would be lost
- Verify the commit hasn’t been pushed to a remote (warns if it has)
The actual undo is a targeted file restore + soft reset:
self.coder.repo.repo.git.checkout("HEAD~1", file_path) # restore each fileself.coder.repo.repo.git.reset("--soft", "HEAD~1") # rewind HEAD/diff — Show changes since the last message boundary. Aider tracks a commit_before_message stack; each new user message pushes the current HEAD onto it. /diff compares the second-to-last entry against current HEAD to show exactly what the agent changed in response to the last request.
Pre-commit Hook Bypass
Section titled “Pre-commit Hook Bypass”Aider respects pre-commit hooks by default. If --no-git-commit-verify is set, it passes --no-verify to skip hooks:
if not self.git_commit_verify: cmd.append("--no-verify")This is useful in repos where pre-commit hooks run linters or tests, and the user wants Aider to commit even when linters haven’t been satisfied yet.
Codex Implementation
Section titled “Codex Implementation”Reference: references/codex/codex-rs/core/src/ | Commit: 4ab44e2c5cc54ed47e47a6729dfd8aa5a3dc2476
Codex does not have an auto-commit system. File edits are applied directly to the working tree without any automatic git staging or committing. The rationale is that Codex’s primary interface is a sandboxed environment where the agent applies a batch of changes and then presents a diff for user review before any commit happens.
This “show-before-commit” model avoids the dirty-commit complexity entirely: the user sees the diff, decides to accept, and then commits manually (or using Codex’s explicit commit tool). It sacrifices the granular per-change commit history that Aider produces in exchange for a cleaner user approval flow.
Codex does interact with git for reading project context — it detects the git root for AGENTS.md discovery and respects .gitignore for file search. But writes go to disk, not to git history.
OpenCode Implementation
Section titled “OpenCode Implementation”Reference: references/opencode/packages/opencode/src/session/ | Commit: 7ed449974864361bad2c1f1405769fd2c2fcdf42
OpenCode also does not auto-commit. File edits from the edit tool are written to disk immediately and tracked in the VFS layer for undo purposes, but they are not committed to git. The session log provides an in-memory undo history (revert to the previous VFS snapshot) that doesn’t require git at all.
This makes OpenCode more portable to non-git environments (a user editing files in a plain directory rather than a repo) but means there’s no persistent undo history across sessions — closing the session discards the in-memory VFS state.
OpenCode does integrate with git for display purposes: it uses git diff output to render colorized diffs in the TUI after each edit, and it can stage files for review. But the commit decision is always left to the user.
Pitfalls and Hard Lessons
Section titled “Pitfalls and Hard Lessons”Commit Message Models Must Be Fast and Cheap
Section titled “Commit Message Models Must Be Fast and Cheap”Using the primary model to generate commit messages is wasteful. Generating “feat: add login validation” requires a 2,000-token diff as input and produces 10 tokens of output. GPT-4 charges per token; calling it for every commit doubles the cost of the session for no quality gain. Dedicate a cheap model to this task. If the cheap model hits its context limit (some very large diffs exceed 4k tokens), truncate the diff rather than upgrading models.
Race Between Formatter and Committer
Section titled “Race Between Formatter and Committer”Auto-formatters that run as file-save hooks (Prettier, Black) can modify a file between the agent writing it and the commit capturing it. If Aider writes file.py, the save hook reformats it, and then the commit captures the reformatted version, the commit diff is now “agent edit + formatter cleanup” mixed together. The solution is to either stage the file immediately after writing (before any hooks can fire) or disable save hooks during AI edit sessions.
The “Aider Commit” Attribution is Not Git-Signed
Section titled “The “Aider Commit” Attribution is Not Git-Signed”Appending (aider) to the author name is metadata only — it’s not cryptographically signed and can be faked by any user with repo access. For security-critical attribution (compliance, audit trails), the proper mechanism is GPG signing, but this requires the user to have a GPG key configured. Aider doesn’t attempt to GPG-sign AI commits; it’s an open issue. OpenOxide should document this limitation explicitly rather than implying the attribution is tamper-proof.
Undo After Push
Section titled “Undo After Push”If the user pushes between Aider turns, the last AI commit is on the remote. Aider warns about this but still allows the undo locally. This creates a diverged history that requires a force push to resolve — which is destructive on shared branches. The safest behavior is to refuse undo after a push and tell the user to revert instead.
Empty Commit When No Files Changed
Section titled “Empty Commit When No Files Changed”If the LLM produces an edit block that resolves to a no-op (the search text equals the replace text, or the “change” is only whitespace that the formatter already normalized), Aider may attempt to commit an empty diff. GitPython raises git.exc.GitCommandError: nothing to commit in this case. The commit call must wrap this case and silently skip the commit rather than surfacing the error to the user.
OpenOxide Blueprint
Section titled “OpenOxide Blueprint”OpenOxide implements auto-commit as an opt-in mode, enabled by default for interactive sessions and disabled for batch/CI use. The implementation lives in openoxide-git.
GitContext Struct
Section titled “GitContext Struct”pub struct GitContext { pub repo: git2::Repository, pub config: CommitConfig, pub session_commits: HashSet<git2::Oid>, // for undo tracking}
pub struct CommitConfig { pub auto_commits: bool, pub dirty_commits: bool, pub attribution: AttributionConfig, pub commit_verify: bool, // whether to run pre-commit hooks}
pub struct AttributionConfig { pub append_to_author: bool, // default true pub append_to_committer: bool, // default true pub co_authored_by: bool, // default false pub message_prefix: bool, // prepend "openoxide: "}Commit Message Generation
Section titled “Commit Message Generation”Commit messages are generated via a dedicated WeakModel call — a separate ModelHandle configured with a small, fast, cheap model:
pub async fn generate_commit_message( model: &WeakModel, diff: &str, context: &str,) -> anyhow::Result<String> { let prompt = format!( "Generate a conventional commit message (max 72 chars) for this diff:\n\n{diff}\n\nContext: {context}" ); let msg = model.complete_simple(&prompt).await?; Ok(msg.trim().trim_matches('"').to_string())}The WeakModel uses gpt-4o-mini or claude-haiku by default. If the diff exceeds 4k tokens, it is truncated to the first and last 1k tokens with a ... [truncated] ... separator.
Dirty File Handling
Section titled “Dirty File Handling”pub async fn pre_edit_check(ctx: &GitContext, path: &Path) -> anyhow::Result<()> { if !ctx.config.dirty_commits { return Ok(()); } if is_dirty(&ctx.repo, path)? { commit_path(ctx, path, CommitKind::UserDirty).await?; } Ok(())}CommitKind::UserDirty generates a commit message from the diff without the AI attribution markers.
Attribution via git2
Section titled “Attribution via git2”Attribution is set on the Signature passed to git2::Repository::commit():
pub fn make_signature( config: &AttributionConfig, model_name: &str, base_name: &str, base_email: &str, kind: CommitKind,) -> git2::Signature<'static> { let name = if config.append_to_author && kind == CommitKind::AiderEdit { format!("{base_name} (openoxide)") } else { base_name.to_string() }; git2::Signature::now(&name, base_email).unwrap()}The Co-authored-by: trailer is appended to the commit message string before passing it to git2::Repository::commit().
Undo is implemented using git2::Repository::reset() with ResetType::Soft:
pub fn undo_last_ai_commit(ctx: &mut GitContext) -> anyhow::Result<()> { let head = ctx.repo.head()?.peel_to_commit()?; if !ctx.session_commits.contains(&head.id()) { bail!("Last commit was not made by OpenOxide — refusing to undo"); } let parent = head.parent(0)?; ctx.repo.reset(parent.as_object(), git2::ResetType::Soft, None)?; ctx.session_commits.remove(&head.id()); Ok(())}The session commit set is stored in memory only — it resets when OpenOxide exits. Cross-session undo requires reading the commit author metadata to identify OpenOxide commits, which is a future enhancement.