Implementation note
1. Introduction
When AI assists with Git work, it becomes easier to manage if the work boundaries defined at the beginning can be turned directly into checks performed before execution. This article describes a small Safety Harness that checks where an operation is about to run, what it is about to affect, and what state the repository is in before any repository-changing action proceeds.
The checks cover the Repository Root, Write Set, Stage Set, Branch, push destination, and Secret Scanner. If any observed value differs from what is expected, the process stops instead of continuing, leaving the state available for human review.
The goal is not to make AI-assisted Git operations completely safe with a single wrapper. The goal is to turn already-defined boundaries into mechanical checks immediately before execution.
2. Defining a boundary and enforcing it are different
Writing “change only this file” or “work only on this branch” in an instruction defines a boundary. It is different from having a mechanism that automatically checks whether the process reading those instructions is actually operating within that boundary.
Before a risky operation, the Harness reads the current state and compares it with the expected state. A different Repository Root, a change outside the Write Set, an overly broad Stage Set, a different push URL, or a Secret Scan that cannot run should cause a stop rather than being presented as success.
Focus of this article: not inventing new boundaries, but moving already-defined Git work boundaries into pre-execution checks.
3. Instruction / Safety Harness / external enforcement boundary
| Layer | Role | What this layer alone does not provide |
|---|---|---|
| Instruction | Tells the human and the AI which Repository Root, Write Set, Branch, and other boundaries are authorized | Does not check whether runtime state actually matches the instruction |
| Safety Harness | Reads state immediately before execution and stops when it differs from expectations | Does nothing if it is bypassed and does not constrain operations performed outside its process |
| External enforcement boundary | Adds separate controls such as CI, protected branches, permissions, and server-side policy | Requires separate design for deployment, operation, and exception handling |
These three layers are not substitutes for one another. A local Harness does not assume that an external enforcement boundary exists; it first checks the state in which it is about to operate.
4. Start with a small Harness
There is no need to automate every Git operation from the beginning. Limiting the checks to a small number of observed values makes failure reasons easier to explain.
- Check the Repository Root.
- Check the Write Set.
- Check the Branch and push URL.
- Explicitly stage authorized paths.
- Check the Stage Set.
- Run the Secret Scan.
- Review the staged diff and move to Human review.
- Perform later operations such as commit only after a separate check.
If a check itself fails, that is not a reason to skip the check and continue. Keeping the Harness small is not a lack of functionality; it is a design condition that keeps stop points explicit.
5. Fail-Closed
Fail-Closed means stopping when a required check cannot be completed instead of continuing because the situation appears safe. It does not prove success; it prevents an unverifiable state from being treated as success.
Define expected values
↓
Read actual state
↓
Cannot compare / mismatch found
↓
STOP
↓
Human review before retry
In particular, if gitleaks is not on PATH, is found but cannot start, or exits with a nonzero code, the Secret Scan is not treated as having passed.
6. Repository Root
Relative paths depend on the current working directory. To operate on the same repository even when the script starts from a subdirectory, first resolve and compare the root returned by Git with the expected root.
$ActualRootRaw = & git rev-parse --show-toplevel 2>$null
if ($LASTEXITCODE -ne 0 -or -not $ActualRootRaw) {
Write-Error 'STOP: Unable to confirm the Repository Root.'
exit 1
}
try {
$ExpectedRoot = (
Resolve-Path -LiteralPath $ExpectedRootPath -ErrorAction Stop
).Path
$ActualRoot = (
Resolve-Path -LiteralPath $ActualRootRaw -ErrorAction Stop
).Path
}
catch {
Write-Error 'STOP: Repository Root path resolution failed.'
exit 1
}
if ($ActualRoot -ne $ExpectedRoot) {
Write-Error "STOP: Repository Root mismatch: $ActualRoot"
exit 1
}
$RepositoryRoot = $ActualRoot
The resolved $RepositoryRoot is shared by subsequent Git operations. If resolution fails, the comparison is not skipped and execution does not continue.
7. Run Git from the Repository Root
After confirming the root, Git calls are fixed to git -C $RepositoryRoot to reduce dependence on the implicit current directory. Arguments are passed as an array rather than by concatenating strings.
function Invoke-GitChecked {
param(
[Parameter(Mandatory = $true)]
[string[]]$GitArgs
)
$Output = & git -C $RepositoryRoot @GitArgs
$ExitCode = $LASTEXITCODE
if ($ExitCode -ne 0) {
throw "STOP: git $($GitArgs -join ' ') failed."
}
return $Output
}
When Git exits nonzero, an empty output is not interpreted as a valid result. The function throws to the caller so the process does not proceed to the next modifying action.
8. Write Set
The Write Set is the set of repository-relative paths authorized to change in the current work unit. It is a descriptive term used in this article, not an official Git term. The check includes not only working-tree changes but also paths already in the index and untracked files not excluded by standard ignore rules.
$ChangedFiles = @(
Invoke-GitChecked -GitArgs @(
'diff',
'--no-renames',
'--name-only'
)
Invoke-GitChecked -GitArgs @(
'diff',
'--cached',
'--no-renames',
'--name-only'
)
Invoke-GitChecked -GitArgs @(
'ls-files',
'--others',
'--exclude-standard',
'--full-name'
)
) |
Where-Object { $_ } |
Sort-Object -Unique
$Unexpected = $ChangedFiles |
Where-Object { $_ -cnotin $AllowedFiles }
if ($Unexpected) {
Write-Error 'STOP: Changes exist outside the Write Set.'
$Unexpected | ForEach-Object { Write-Host " $_" }
exit 1
}
--no-renames keeps source and destination paths from being collapsed into a rename. This leaves the original path visible to the check even when a rename moves a file from outside the Write Set into it. The comparison uses the case-sensitive -cnotin operator to avoid ambiguous path matches.
AllowedFiles should use the repository-root-relative path format returned by Git, with / separators. For non-ASCII paths, the default core.quotePath setting can produce representation differences.
9. Explicit staging
Staging should name the authorized paths explicitly. Rather than relying on an operation that stages the whole working tree, the command itself preserves the same boundary as the Write Set.
# Example: explicitly stage authorized paths
git add -- <allowed-file-1> <allowed-file-2>
This step only names what should be staged; it does not declare PASS. The actual contents of the index are checked in the Stage Set section that follows.
10. Stage Set
The Stage Set is the set of paths currently present in the index. Even if the Write Set is correct, another file can become staged before the next operation, so the Stage Set is treated as an independent observed value.
$StageSet = @(
Invoke-GitChecked -GitArgs @(
'diff',
'--cached',
'--no-renames',
'--name-only'
)
) |
Where-Object { $_ } |
Sort-Object -Unique
$UnexpectedStaged = $StageSet |
Where-Object { $_ -cnotin $AllowedFiles }
if ($UnexpectedStaged) {
Write-Error 'STOP: Paths outside the Stage Set boundary are staged.'
exit 1
}
Write-Host 'PASS: Allowed files staged.'
The Stage Set check always uses git diff --cached --no-renames --name-only. This avoids confusing unstaged working-tree state with the index that will be used by the next operation.
11. Branch
The Branch is repository state that can change the meaning of the same file set. The authorized Branch name is made explicit and compared with the current Branch.
$CurrentBranch = Invoke-GitChecked -GitArgs @(
'symbolic-ref',
'--short',
'HEAD'
)
if ($CurrentBranch -ne $ExpectedBranch) {
Write-Error "STOP: Branch mismatch: $CurrentBranch"
exit 1
}
A detached HEAD is not guessed into a normal Branch name, and the script does not automatically switch away from a different Branch. A Branch mismatch is an input that causes a stop and review.
12. Push URL
The push destination does not necessarily match the fetch destination. When confirming the write destination, read the remote's push URL directly.
$PushUrls = @(
@(
Invoke-GitChecked -GitArgs @(
'remote',
'get-url',
'--push',
'--all',
$RemoteName
)
) | Where-Object { $_ }
)
if (
$PushUrls.Count -ne 1 -or
$PushUrls[0] -cne $ExpectedPushUrl
) {
Write-Error "STOP: Push URL mismatch: $($PushUrls -join ', ')"
exit 1
}
The check uses git remote get-url --push --all <remote>. It passes only when exactly one URL is returned and that URL exactly matches ExpectedPushUrl. Zero URLs, multiple URLs, and mismatches all cause STOP. This small Harness does not expand into a policy engine that permits multiple push URLs.
13. Secret Scanner
The Secret Scanner distinguishes between being able to run a scan and the scan result actually passing. If gitleaks is absent, cannot start, or exits nonzero, the Harness does not proceed as if the scan passed.
$GitleaksCommand = Get-Command gitleaks `
-CommandType Application `
-ErrorAction SilentlyContinue
if (-not $GitleaksCommand) {
Write-Error 'STOP: gitleaks cannot be executed.'
exit 1
}
$GitleaksExitCode = $null
try {
& $GitleaksCommand.Source `
git `
--staged `
--redact `
$RepositoryRoot
$GitleaksExitCode = $LASTEXITCODE
}
catch {
Write-Error "STOP: gitleaks execution failed: $($_.Exception.Message)"
exit 1
}
if ($null -eq $GitleaksExitCode -or $GitleaksExitCode -ne 0) {
Write-Error 'STOP: Secret scan did not pass.'
exit 1
}
Write-Host 'PASS: Secret Scan OK'
--redact masks the values of detected secrets in logs or output. The findings themselves are still reported. This does not mean the Secret Scanner can detect every secret; its coverage and configuration must be evaluated separately.
The gitleaks git ... form used in this example assumes the CLI form introduced in Gitleaks v8.19.0 and later.
14. Keep push outside the normal Harness
It is easier to explain the boundary when checking local changes and pushing to an external repository are not packed into the same automatic path. The normal Harness can stop at “the expected Stage Set and Secret Scan passed,” while push remains behind a separate, explicit approval and verification boundary.
This prevents a local Harness PASS from being expanded into evidence that writing to the external repository was authorized or that push succeeded.
15. Limits of a Soft Harness
Soft Harness is also a descriptive term used in this article, not a standard Git term. Here it means a mechanism such as a PowerShell wrapper or local script that checks state and stops when invoked.
A Soft Harness makes it harder to use a normal path that bypasses the checks. It does not constrain someone from directly invoking another command, using another working tree, editing the script, or changing an external enforcement boundary.
For that reason, a Soft Harness should not be treated as the security boundary itself. Required enforcement belongs in separate layers such as CI, protected branches, repository permissions, and approval workflows.
16. What the Harness makes easier to prevent
| Check | Differences it can readily expose | What it cannot detect |
|---|---|---|
| Repository Root | Wrong repository, nonexistent expected root, dependence on a subdirectory | Every semantic error inside the root |
| Write Set | Unauthorized tracked / staged / untracked paths and unexpected rename sources | Items outside the enumeration conditions, such as ignored files |
| Stage Set | Unauthorized paths mixed into the index | Whether the reviewed content itself is correct |
| Branch / Push URL | Using the wrong Branch or write destination | The complete state of approvals and protections on the remote side |
| Secret Scanner | The scanner cannot start, fails, or reports something within its detection scope | Every secret and every possible leakage path |
“Makes easier to prevent” does not mean “prevents everything.” The value of the Harness is that certain operational mix-ups become observable differences that can cause a stop before a change or push.
17. Positioning of the code examples
The PowerShell examples shown here are a minimal structure for explaining the Write Set check, root resolution, Git invocation, Stage Set, Branch, push URL, and the fail-closed gitleaks path. In a real repository, fix the authorized paths, Branch, remote name, push URL, and scanner settings to match the approval for that work.
Write Set, Stage Set, Safety Harness, and Soft Harness are descriptive terms used in this article. Although terms such as write set and harness are used in other fields, the definitions here should not be treated as official Git terminology.
18. Runtime verification
The Fail-Closed paths in these examples were checked in five cases using isolated temporary Git repositories. This records that the example code stopped under the tested conditions; it does not prove safety in every environment.
The table below shows that the Harness stopped as expected under each condition.
| Case | PowerShell 7 | Windows PowerShell 5.1 |
|---|---|---|
| 1. outside untracked | STOP as expected | STOP as expected |
| 2. outside→inside rename | STOP as expected | STOP as expected |
| 3. gitleaks unavailable | STOP as expected | STOP as expected |
| 4. invalid root | STOP as expected | STOP as expected |
| 5. gitleaks found but cannot start | STOP as expected | STOP as expected |
Versions: Git 2.54.0.windows.1 / PowerShell 7.6.5 / Windows PowerShell 5.1.26100.9444
- This verification is limited to the five cases above.
- It does not cover every Git configuration.
- It is not verification of a finished security product.
- Known out-of-scope conditions such as ignored files remain as described in the article.
19. Conclusion
A Safety Harness for AI-assisted Git operations does not need to begin as a large automation platform. Confirming the Repository Root, running Git from that root, explicitly comparing the Write Set and Stage Set, and checking the Branch, push URL, and Secret Scanner can already increase the number of operational mix-ups that can be stopped before execution.
The important point is not to convert an unverifiable state into PASS. Write Set, Stage Set, Safety Harness, and Soft Harness are descriptive terms used in this article; they should be applied within their stated scope rather than mistaken for official Git terminology or a universal security boundary.
Finally, a local Harness PASS does not mean that commit, push, deploy, or runtime verification is complete. Moving to the next boundary should require a separate approval and check for that operation.
References
21. References
- Git Documentation: git-rev-parse / git-diff / git-add / git-ls-files / git-remote
- Microsoft Learn: about_Automatic_Variables / about_Try_Catch_Finally
- Gitleaks official repository: gitleaks/gitleaks
These sources are used to confirm Git path, diff, and remote inspection; PowerShell exit-code and exception behavior; and the official implementation of the Secret Scanner.