Audit What a Git Repo Can Run
Build a PowerShell script that reports every command a git repo can execute.
Introduction
30 Second Summary
When you download a project and open it in your editor, Git can run commands that someone else configured inside that folder. Most of the time you never know it happened.
In this project, you will build a PowerShell script called git-trust-report.ps1 that answers one question for any project folder: what is it allowed to run? You will prove the script works by planting test settings, catching them, and cleaning them up.
What You'll Build
You run one command against a downloaded project folder, and in under a second you see exactly what git is configured to execute on your machine.
By the end of this project, you'll have:
- A trust report script that shows what any project folder can execute: nine command-executing config settings, local layer values, and active hooks.
- A tested script, proven by planting a setting, catching it, planting a hook, catching it, removing both, and confirming clean.
- A clear connection between your script and Cursor's August 2026 git extension change, which overrides four of these settings during agent operations.
- An optional Secret Mission to push your skills further.
Do I need anything before starting?
This project runs on Windows using PowerShell (built in) and Git for Windows (2.47 or later). Basic command line familiarity helps but is not required.
Before We Start
Before you start building, take a moment to lock in what you're about to create and why. You are going to write a single PowerShell script that answers one question about any Git project folder: what is this folder allowed to run?
Create a Test Repo
The script you are about to build needs a Git repository to inspect. A fresh repo with known contents gives you a controlled starting point where you can plant settings, detect them, and clean them up.
In this step, you will create a test folder called git-audit-lab, initialize it as a repository, and make one commit. This gives you a known-clean baseline with no local config settings and no hooks.
In this step, get ready to:
- Create the git-audit-lab project folder and initialize a Git repository.
- Add a file, stage it, and make your first commit.
Create the project folder and initialize a repo
- In your PowerShell window, move to your Desktop by running this command:
cd ~/Desktop
- Create the project folder and enter it by running these commands:
mkdir git-audit-lab
cd git-audit-lab
- Initialize a new Git repository by running:
git init
You should see a message confirming the repository was initialized.
What did git init create?
This command creates a hidden .git folder inside git-audit-lab. This folder is where Git stores everything about the repository: commits, branches, configuration, and hooks.
Inside .git/hooks, Git creates .sample files by default. These are inactive templates, not live hooks. The trust report script you build later will skip them.
Create a file and make the first commit
- Create readme.txt with a line of text by running this command:
Set-Content readme.txt "Git audit lab"
- Stage the file and commit it by running these commands:
git add readme.txt
git commit -m "Initial commit"
You should see a message showing one file changed and one insertion.
Commit command failed?
- If Git asked you to set your identity (name and email), follow the instructions in the error message to configure them, then run the git commit command again.
- If you see a message about nothing to commit, make sure you ran git add readme.txt before git commit.
Help me figure out why my git commit is failing.
Before you run the next command, how many commits do you expect to see in the log?
- Verify the commit history by running:
git log --oneline
You should see exactly one line: a short commit hash followed by Initial commit. The repo is ready.
Not seeing one commit?
- If you see no output, your commit did not go through. Scroll up to check whether the git commit command showed an error, and resolve it using the troubleshooting callout above.
- If you see more than one commit, you may have run git commit more than once. That is fine for this project.
Help me figure out why git log is not showing my commit.
Your test repo is clean and ready. Next, you will write the trust report script and run it against this repo.
Write the Script and Run It
You have a clean repo with one commit. Now you need a single command that answers the question: what is this project folder allowed to run?
In this step, you will write a PowerShell script that lists nine Git config settings whose value is a command, checks the local layer for each one, and lists any live hook scripts. The first run will show a clean report. That is correct for this repo, but it is not proof the script works. A clean first run is an untested script.
In this step, get ready to:
- Create the script file and define the nine command-executing settings with reasons.
- Add the local layer check and the hooks check.
- Run the full report and see a clean result.
Create the script file and define the settings
The script needs to know which settings to look for. Nine git config keys hold a command rather than a plain value. Git runs that command during ordinary operations like committing, pushing, or diffing.
- Create the script file by running this command:
New-Item git-trust-report.ps1
- Confirm the file exists by running:
ls
You should see git-trust-report.ps1 and readme.txt in the output.
- Open the file in Notepad by running:
notepad git-trust-report.ps1
Why Notepad?
This project uses only PowerShell and Git. Notepad ships with every Windows install, so no extra tools are needed. If you prefer a different text editor, use that instead.
- Paste the following code into the empty file to define the script header and the nine settings:
param(
[string]$Path = "."
)
$repo = (Resolve-Path $Path).Path
$gitDir = Join-Path $repo ".git"
if (-not (Test-Path $gitDir -PathType Container)) {
Write-Host "Not a git repository: $repo"
exit 1
}
$settings = @(
@{ Key = "core.pager"; Reason = "Runs on long output (git log, git diff, git show)" }
@{ Key = "core.editor"; Reason = "Runs when a commit message or tag needs editing" }
@{ Key = "core.sshCommand"; Reason = "Replaces ssh during push and fetch" }
@{ Key = "core.fsmonitor"; Reason = "Runs on git status, git diff, git add" }
@{ Key = "diff.external"; Reason = "Replaces the built-in diff with an external program" }
@{ Key = "credential.helper"; Reason = "Runs when git needs a username or password" }
@{ Key = "gpg.program"; Reason = "Runs on signature creation and verification" }
@{ Key = "core.hooksPath"; Reason = "Points git at a directory of executable hook scripts" }
@{ Key = "init.templateDir"; Reason = "Copies its contents (including hooks) into every new repo" }
)
What does this code do?
The param() block accepts an optional -Path argument so you can point the script at any repo. It defaults to . (the current folder).
The script resolves the full path, checks that a .git directory exists, and exits early if it does not. The $settings array holds the nine keys that git documents as executing an external program, each paired with a plain-language reason.
- Directly below the settings array, paste the hook names list and the code that prints the report header and settings:
$knownHooks = @(
"applypatch-msg", "pre-applypatch", "post-applypatch",
"pre-commit", "pre-merge-commit", "prepare-commit-msg",
"commit-msg", "post-commit", "pre-rebase",
"post-checkout", "post-merge", "pre-push",
"pre-receive", "update", "proc-receive",
"post-receive", "post-update", "reference-transaction",
"push-to-checkout", "pre-auto-gc", "post-rewrite",
"sendemail-validate", "fsmonitor-watchman", "post-index-change"
)
Write-Host ""
Write-Host "git trust report"
Write-Host "================"
Write-Host "Repository: $repo"
Write-Host ""
Write-Host "Settings whose value is a command"
Write-Host ""
foreach ($s in $settings) {
Write-Host (" {0,-24}{1}" -f $s.Key, $s.Reason)
}
What does this code do?
The $knownHooks array lists every standard hook name from the git documentation. You define it now so it is ready when the hooks section needs it later.
The foreach loop prints each of the nine settings with its reason, formatted as a two-column table. This is the reference section of the report.
- Save the file in Notepad by pressing Ctrl+S.
- Switch back to PowerShell and run the script to check the first section works:
.\git-trust-report.ps1
You should see the report header with the repo path, followed by all nine settings with their reasons.
Seeing a "cannot be loaded" error?
PowerShell's execution policy may block scripts. Run Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned and try again. This allows locally created scripts to run.
If you see no output at all, make sure you saved the file in Notepad before switching to PowerShell.
Why can't I run my PowerShell script?
Add the local layer check
The reference section lists the nine settings. Now the script needs to check which ones the local layer actually sets. The local layer is the .git/config file inside the project folder.
- Switch back to Notepad. At the bottom of the file, below the foreach loop, paste the local layer section:
Write-Host ""
Write-Host "Local layer (.git/config)"
Write-Host ""
$localCount = 0
foreach ($s in $settings) {
$val = $null
$val = git -C $repo config --local --get $s.Key 2>$null
if ($LASTEXITCODE -eq 0 -and $null -ne $val -and $val -ne "") {
Write-Host (" {0,-24}{1}" -f $s.Key, $val)
$localCount++
}
}
if ($localCount -eq 0) {
Write-Host " (none)"
}
What does this code do?
The loop calls git config --local --get for each of the nine settings. The --local flag is critical. It tells git to read only .git/config, not the global or system config files.
Without --local, git reads all layers together and you cannot tell which layer set a value. The script only cares about what this project folder controls.
- Save the file with Ctrl+S.
- Switch back to PowerShell and run the script again:
.\git-trust-report.ps1
The output now includes the local layer section showing (none). That is correct. You have not set any of the nine keys in this repo's local config.
Seeing a setting you did not plant?
If a setting appears under the local layer that you did not set yourself, the script may be missing the --local flag. Without it, git falls through to your global and system config. Check that the git config call in your loop includes --local --get.
Why is my script showing settings I didn't set?
Add the hooks check and run the full report
Settings are only half the picture. A hook is a script that git runs by itself at a set moment, like just before a commit. Hook scripts live in the .git/hooks directory by default.
- Switch back to Notepad. At the bottom of the file, below the local layer section, paste the hooks directory check:
Write-Host ""
Write-Host "Hooks"
Write-Host ""
$hooksDir = Join-Path $gitDir "hooks"
$customPath = git -C $repo config --local --get core.hooksPath 2>$null
if ($LASTEXITCODE -eq 0 -and $customPath) {
if ([System.IO.Path]::IsPathRooted($customPath)) {
$hooksDir = $customPath
} else {
$hooksDir = Join-Path $repo $customPath
}
Write-Host " Directory: $customPath (set by core.hooksPath)"
} else {
Write-Host " Directory: .git/hooks"
}
What does this code do?
The hooks directory defaults to .git/hooks. But if core.hooksPath is set in local config, git looks there instead. The script checks for this override and follows it. The same setting that redirects hooks is itself one of the nine command-executing settings.
- Directly below that block, paste the hook file scan and the closing blank line:
Write-Host ""
$hookCount = 0
if (Test-Path $hooksDir -PathType Container) {
foreach ($hook in $knownHooks) {
$hookFile = Join-Path $hooksDir $hook
if (Test-Path $hookFile -PathType Leaf) {
Write-Host " $hook"
$hookCount++
}
}
}
if ($hookCount -eq 0) {
Write-Host " (none)"
}
Write-Host ""
What does this code do?
The loop checks for each known hook name in the hooks directory. It looks for exact file names like pre-commit and post-merge. Files ending in .sample are inactive templates that git creates on git init. Because the loop checks exact names, those templates are correctly skipped.
- Save the file with Ctrl+S.
The script is complete. Before you run it, what do you expect to see under the local layer and hooks sections?
- Switch back to PowerShell and run the full report:
.\git-trust-report.ps1
The output shows the nine settings with reasons, (none) under the local layer, and (none) under hooks. The report is correct. But it is untested. You have not yet proven it can detect a setting that is present.
✔️ Awesome, I've got everything!
Great. Double check you have saved git-trust-report.ps1 in Notepad.
ⓧ I'd like to double check the full code
The full file is long. Scroll through git-trust-report.ps1 in Notepad and confirm these sections appear in order:
- The param() block, path resolution, and .git check.
- The $settings array with nine key-reason pairs.
- The $knownHooks array listing standard hook names.
- The report header and settings print loop.
- The local layer section with git config --local --get for each setting.
- The hooks section with directory detection and the hook file scan loop.
Output missing a section?
If the local layer or hooks section does not appear in the output, check that you pasted each block at the bottom of the file, not inside an existing block. Each section should follow the previous one, not be nested inside it.
If you see an error about an unexpected token or missing bracket, look for an unmatched ) or }. The settings array and known hooks array both end with ).
My git-trust-report.ps1 output is missing sections or showing errors.
The report reads clean. Every setting says (none) and no hooks are live. Next, you will prove the script actually works by planting a setting and checking that it gets caught.
Plant a Setting and Catch It
Your script reported clean on a clean repo in the previous step. That proves the script runs without errors. It does not prove the script works.
The test that matters is: plant a known setting in the local layer, run the script, and check that it catches it. If it does, the script detects what a project folder controls. If it does not, the script is reading the wrong layer.
In this step, get ready to:
- Plant a command-executing setting in the local config layer.
- Run the script and confirm it detects the planted setting.
Plant a setting in the local layer
core.fsmonitor is one of the nine command-executing settings your script checks. Git runs its value during git status, git diff, and git add. This is also the setting that Cursor's override sets to false.
- Plant core.fsmonitor in the local layer by running this command:
git config --local core.fsmonitor echo
What does this command do?
- git config --local writes the setting into .git/config inside this project folder only. It does not touch the global or system config files.
- core.fsmonitor echo sets the filesystem monitor to echo, a harmless command that produces output but changes nothing.
- Confirm the setting was written by running:
git config --local --get core.fsmonitor
You should see echo printed back. The setting is now planted in this repo's local layer.
Run the script and catch the planted setting
Before you run the script, what do you think the local layer section will show this time? Will it still say (none), or will it catch the setting you just planted?
- Run the script by running this command:
.\git-trust-report.ps1
Under the local layer section, core.fsmonitor now appears with its value echo. This is the passing test. The script detected a setting that the project folder controls.
What are the two failure modes?
Two failures are worth naming, because each one tells you something different about what went wrong.
- If planting a setting changes nothing in the report, the script is reading the wrong layer. Check that the script uses --local in the git config call.
- If a clean repo reports a setting you did not plant, the script is reading the global layer instead of just the local layer. Without the --local flag, git config --get reads all layers (system, global, and local together), and you cannot tell which layer set the value.
Don't see core.fsmonitor in the output?
- Confirm the setting was planted by running git config --local --get core.fsmonitor. If it does not return echo, re-run the plant command from the previous substep.
- Open git-trust-report.ps1 and check that the local layer loop uses --local --get in the git config call. Without --local, the script may read the wrong layer.
- Make sure you are running the script from inside the git-audit-lab folder. The script defaults to the current directory.
Help me debug why my git-trust-report.ps1 script is not detecting the planted core.fsmonitor setting.
Leave core.fsmonitor planted. You will use it again in the next step.
Your script just passed its first real test. Next up, you will plant a hook, verify that the script catches it too, then clean both and confirm the report goes quiet.
Plant a Hook, Then Clean Both
In the last step, you planted core.fsmonitor in the local layer and watched the script catch it. That proved the settings side of the report works.
The script checks two surfaces: settings and hooks. Settings are proven. Now you need to prove hooks work the same way, then remove both findings and confirm the report goes clean. The full test cycle is: detect presence, then detect absence.
In this step, get ready to:
- Plant a pre-commit hook and confirm the script detects it alongside the setting from the previous step.
- Remove both findings and confirm the report goes clean.
Plant a pre-commit hook
You have already proven that the script detects a planted setting. Now you need to prove it detects a hook too.
- Create a pre-commit hook file by running this command:
Set-Content -Path .git/hooks/pre-commit -Value "exit 0"
What is a pre-commit hook?
A hook is a script Git runs by itself at a set moment. The pre-commit hook runs just before git records a commit. Git for Windows looks for this file by name in the hooks directory.
The exit 0 content is a harmless shell command that does nothing and succeeds. You only need the file to exist so the script has something to detect.
- Run the script to check if it picks up the new hook:
.\git-trust-report.ps1
Under hooks, pre-commit now appears. Under the local layer, core.fsmonitor still shows echo from the previous step. The report shows both findings at once.
Don't see pre-commit under hooks?
- Check that the file is named exactly pre-commit with no extension. If your file is called pre-commit.txt, git will not find it.
- Confirm the file is inside .git/hooks/. Run ls .git/hooks/ to list the directory and look for pre-commit among the .sample files.
- Still stuck? Why isn't my pre-commit hook showing up in the trust report?
Remove both findings and verify clean
Detecting presence is half the test. Detecting absence completes it. You need to remove both findings and confirm the report goes quiet.
- Unset the planted setting by running this command:
git config --local --unset core.fsmonitor
- Delete the hook by running this command:
Remove-Item .git/hooks/pre-commit
Before you run the script, what do you expect to see under the local layer and hooks sections now that both have been removed?
- Run the script one final time:
.\git-trust-report.ps1
The local layer shows (none). Hooks shows (none). The project reports clean.
What does known-clean mean?
This is known-clean, not assumed-clean. You have seen the script detect both kinds of finding (a setting and a hook) and then go quiet when both are removed.
Your first clean run in Step 2 was an untested script reporting on an empty repo. This clean run is a tested script reporting on a repo you deliberately dirtied and then cleaned. The difference is proof.
Still seeing a finding after cleanup?
- If core.fsmonitor still appears, the unset command may not have worked. Run git config --local --list to see everything in your local config. If core.fsmonitor is still listed, run the unset command again.
- If pre-commit still appears under hooks, the file was not deleted. Run ls .git/hooks/ and check whether pre-commit is still present.
- If you see settings you did not plant (like core.editor or credential.helper), the script may be reading your global layer instead of only the local layer. Check that the script uses the --local flag in every git config call.
- Need more help? My trust report still shows findings after I removed both the setting and the hook. Help me debug.
Run it on a real project next
The script is ready to run against any git repo. Try it on a project you downloaded rather than one you wrote. That is the case it exists for.
You can also add whatever else your own tools run from configuration: a build system, a package manager, a task runner. The shape does not change. Only the list does.
Secret mission
Widen the Lens
The nine settings and the hooks directory are the direct surface. Three indirect vectors exist that the basic report does not cover. Can you extend your script to catch them?
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely locally, so there are no ongoing costs or cloud resources to worry about. Decide whether to keep your files to reference later or delete them entirely.
Resources you used:
- git-audit-lab folder on your Desktop, containing git-trust-report.ps1, readme.txt, and the .git directory.
Keep everything running
No action needed. Choose this if you plan to keep running git-trust-report.ps1 against other project folders or want to extend the script further.
- Your git-audit-lab folder stays on your Desktop. You can copy git-trust-report.ps1 into any other project folder and run it there.
- There are no running processes, background services, or cloud resources associated with this project.
Pause - I'll come back to this later
Since this project is entirely local with no running processes, pausing is the same as keeping. Your files sit on disk and consume no memory or CPU.
- Close your PowerShell window when you are done.
- When you are ready to return, open PowerShell, navigate back to the git-audit-lab folder on your Desktop, and pick up where you left off.
Delete - I don't want to use this again
Remove the project folder entirely. This deletes the script, the test repo, and all git history inside it.
- In PowerShell, move out of the git-audit-lab folder by running:
cd ~/Desktop
- Delete the git-audit-lab folder by running:
Remove-Item -Recurse -Force git-audit-lab
- Confirm the folder is gone by running:
ls git-audit-lab
You should see an error stating the path does not exist. The project is fully removed.
Nice Work!
Nice Work!
Nice work! You built a single PowerShell script that reads any project folder and answers one question: what is it allowed to run?
You've learned how to:
- Build a single script that reports the trust surface of any Git repo: nine command-executing config settings, their local layer values, and live hooks.
- Prove a script works through a plant-detect-clean cycle. A clean first run is an untested script. Planting a setting, detecting it, removing it, and seeing the report go clean is the real test.
- Connect config auditing to a real event where Cursor's git extension overrode four settings via GIT_CONFIG_* environment variables, silently disabling developer hooks.
- Tackled an optional Secret Mission to push your skills further.
Ready to quiz yourself?