In the previous FileEditTool chapter, we gave claudeCode the "hands" to modify your code files. But anyone who has written code knows that saving a file is only half the battle.
The other half is Version Control.
If the AI makes a mistake, we need to be able to undo it. If the AI makes 10 changes, we might want to group them into a "Commit." This is where Git Integration comes in.
Git is complex. It involves branches, remotes, staging areas, and sometimes complex folder structures called "worktrees."
If we just blindly ran git commands in the terminal, we might crash if the user doesn't have Git installed, or if we are inside a folder that isn't actually a repository.
The Git Integration module is a set of utilities that answers three critical questions safely:
Imagine you ask claudeCode: "Summarize my recent changes."
To answer this, the application needs to:
.git folder lives).Without this integration, the AI is blind to the project's history.
Every Git project has a "root" folder. This is where the hidden .git directory lives.
src/utils/, the root is ../../.Advanced Git users use "Worktrees." This allows you to have the same project checked out in two different folders at the same time (e.g., one folder for "main" branch, one for "feature" branch). Our integration handles this automatically so the AI doesn't need to know the difference.
Knowing this is crucial for the Auto-Mode Classifier to decide if a task is "finished."
These utilities are usually helper functions imported from utils/git.ts.
Here is how we determine where the project starts.
import { findGitRoot } from './utils/git';
import { getCwd } from './utils/cwd';
// 1. Get where the user currently is
const currentDir = getCwd();
// 2. Walk up the tree to find .git
const root = findGitRoot(currentDir);
if (!root) {
console.log("Not in a git repository!");
}
Explanation: findGitRoot looks at the current folder. If it doesn't see .git, it checks the parent folder, and so on, until it hits the top of the drive.
The AI often needs to know if the workspace is "Clean."
import { getIsClean } from './utils/git';
async function checkStatus() {
// Returns true if no files are modified
const isSafe = await getIsClean();
if (!isSafe) {
console.log("You have uncommitted changes.");
}
}
Explanation: This wraps git status. If Git returns output, it means things are "dirty" (changed).
To help the AI understand the project context (e.g., "Is this an open source library?"), we fetch the remote origin.
import { getRemoteUrl } from './utils/git';
async function identifyProject() {
const url = await getRemoteUrl();
// Output might be: "git@github.com:anthropics/claude-code.git"
return url;
}
The Git Integration doesn't reinvent Git. It acts as a translator. It runs standard Git commands in the background and converts the messy text output into nice JavaScript objects.
Here is the flow when checking for changed files:
Let's look at utils/git.ts to see how this is actually implemented.
This logic mimics how Git itself finds the root directory.
// utils/git.ts (Simplified)
import { statSync } from 'fs';
import { join, dirname } from 'path';
const findGitRootImpl = (startPath) => {
let current = startPath;
// Loop until we reach the top of the drive
while (current !== root) {
// Check if .git exists here
if (pathExists(join(current, '.git'))) {
return current; // Found it!
}
// Move up one level
current = dirname(current);
}
return null;
};
Explanation: We use a while loop to climb up the directory tree. We check for .git at every step. We use memoization (caching) in the real code so we don't scan the hard drive every single time.
This is the complex part. Sometimes .git isn't a folderβit's a file pointing to another folder.
// utils/git.ts (Simplified Logic)
const resolveCanonicalRoot = (gitRoot) => {
// Read the .git item
const gitContent = readFileSync(join(gitRoot, '.git'), 'utf-8');
// If it starts with "gitdir:", it's a worktree!
if (gitContent.startsWith('gitdir:')) {
// Follow the path to find the REAL git folder
const realPath = gitContent.slice(8).trim();
return resolve(realPath);
}
return gitRoot;
};
Explanation: Worktrees are basically "shortcuts." This function follows the shortcut to find where the actual history database is stored. This ensures that even if you are in a worktree, the AI knows the real project identity.
We use a helper to run the commands safely without crashing the app if Git fails.
// utils/git.ts
import { execFileNoThrow } from './execFileNoThrow.js';
export const getChangedFiles = async () => {
// Run: git status --porcelain (machine readable format)
const { stdout } = await execFileNoThrow(
'git',
['status', '--porcelain']
);
// Turn text output into an array of filenames
return stdout
.split('\n')
.map(line => line.substring(3)); // Remove "M " prefix
}
Explanation: execFileNoThrow ensures that if Git crashes or isn't installed, our app doesn't crash. It just returns an error code which we handle gracefully.
This chapter provides the "Project Awareness" needed for advanced features:
BashTool is what allows the AI to actually run git commit or git push.findGitRoot to limit the AI's access. We might say: "You can only edit files inside this Git repository."
You have learned that Git Integration is the compass of claudeCode. It tells the application where the project boundaries are, what has been changed, and how to navigate complex setups like worktrees.
Now that the AI can read the project status, what if it needs to run other commands? Like listing directories, installing packages, or running tests?
Generated by Code IQ