GrokPatrol: How to Build Forensic Tools That Can't Lie to You
Hook
Most security tools promise they won't phone home. GrokPatrol proves it at compile time by ensuring no network packages are linked into the binary—a security property you can verify by running go list -deps yourself.
Context
In late 2024, xAI's Grok Build CLI was found to be silently collecting and uploading entire git repositories to xAI infrastructure, ostensibly for context but without adequate disclosure. The incident hit a nerve in the developer community because the exfiltration was passive—no warning prompts, no consent dialogs, just background uploads while you worked. By the time teams realized what happened, the immediate incident response question wasn't 'how do we prevent this' but 'did it already happen to us, and what secrets did it take?'
Network-based detection was useless for this post-mortem scenario. If you weren't running packet capture when the CLI uploaded your repos, you had no live traffic to analyze. What remained were filesystem artifacts: the CLI's logs in ~/.grok/logs/, staged upload queues waiting to sync, and—most critically—the git repositories themselves, which might contain deleted secrets still alive in history. GrokPatrol emerged as a purpose-built forensic scanner to answer one specific question: 'What evidence remains on disk that Grok collected or uploaded my code, and which secrets were exposed?' It's not a general-purpose security tool. It's a surgical instrument for a specific incident.
Technical Insight
The brilliance of grokpatrol lies in how it uses architectural constraints to guarantee its own safety properties. This matters because forensic tools operate in hostile environments—you're running them on potentially compromised machines, collecting sensitive evidence, and any mistake could leak the very secrets you're trying to inventory.
Every filesystem operation flows through a centralized gate in pkg/scan/filesystem.go that enforces read-only access using O_RDONLY flags. More importantly, the dependency graph is explicitly verified to exclude network packages. The project includes a make verify-deps target that walks go list -deps output and fails the build if it finds net/http, net, or any networking primitives. This isn't runtime behavior you promise to follow—it's a compile-time property. If grokpatrol phones home, it means someone modified the source code and rebuilt it, which breaks the attestation signatures (more on that in a moment).
The evidence collection model is equally clever. Instead of parsing git internals directly, grokpatrol shells out to an allowlist of safe git commands to enumerate repository state:
// From pkg/scan/git.go (conceptual reconstruction)
var allowedGitCommands = map[string]bool{
"rev-list": true,
"rev-parse": true,
"ls-tree": true,
"diff-index": true,
}
func enumerateGitSecrets(repoPath string) ([]string, error) {
// Get all objects reachable from HEAD
headObjects := exec.Command("git", "rev-list", "--objects", "HEAD")
headObjects.Dir = repoPath
// Get current working tree state
workingTree := exec.Command("git", "ls-tree", "-r", "HEAD")
workingTree.Dir = repoPath
// Deleted files = in history but not in working tree
// These are the secrets developers think they removed
deletedPaths := setDifference(headObjects, workingTree)
// Return paths and blob IDs WITHOUT running git cat-file
// We report WHAT exists, not the content itself
return deletedPaths, nil
}
The critical insight is what this code doesn't do: it never invokes git cat-file to extract file contents. It identifies which blobs contain potential secrets (deleted .env files, removed config.json with API keys) and reports their blob IDs and paths, but the evidence model has no fields for file contents. This structural limitation means even if an attacker compromised the grokpatrol binary itself, the most it could exfiltrate is metadata—not the actual secrets.
The execution model is a linear pipeline of specialized evidence collectors:
// Conceptual flow from cmd/scan/main.go
type EvidenceAccumulator struct {
GrokLogFiles []LogEvidence
UploadQueues []QueueEvidence
DeletedSecrets []SecretMetadata
Verdict ScanVerdict
BlindSpots []string
}
func runScan(targetPath string) EvidenceAccumulator {
evidence := EvidenceAccumulator{}
// Each collector operates independently
evidence.GrokLogFiles = scanGrokLogs("~/.grok/logs")
evidence.UploadQueues = scanUploadQueue("~/.grok/upload_queue")
evidence.DeletedSecrets = scanGitHistory(targetPath)
// Track what we couldn't see
if permissionDenied("~/.grok/logs") {
evidence.BlindSpots = append(evidence.BlindSpots, "macOS TCC blocked log access")
}
// Verdict degrades to INDETERMINATE if ANY collector failed
evidence.Verdict = computeVerdict(evidence)
return evidence
}
The 'degraded scan' design is forensically honest in a way most tools aren't. If macOS Transparency, Consent, and Control (TCC) blocks access to the ~/.grok/logs directory, grokpatrol doesn't silently skip it and report 'CLEAN'. It changes the verdict to 'INDETERMINATE' and explicitly lists the blind spot in the output. This acknowledges the epistemic limits of what the tool can know.
Finally, the release process uses sigstore attestations to create a tamper-evident audit trail. Every binary is signed during the GitHub Actions build, and the signature is recorded in an append-only transparency log (Rekor). Even if an attacker compromised the GitHub repository tomorrow and pushed a backdoored version, they couldn't forge attestations for past releases—the transparency log is already written. Responders can verify that the grokpatrol binary they downloaded was built by the legitimate CI workflow from a specific git commit, not by an attacker's machine.
Gotcha
GrokPatrol is surgical, which means it's also brittle. It's hardcoded to detect exactly one thing: evidence of the Grok Build CLI incident. The log paths (~/.grok/logs/), upload queue structures, and even the Google Cloud Storage bucket name (gs://grok-code-session-traces) are all baked into the detection logic. If xAI changes the directory structure or you're investigating a different supply-chain tool, grokpatrol gives you nothing. It's not a framework—it's a single-purpose artifact detector.
The git history analysis has meaningful blind spots. It only examines objects reachable from HEAD, which means secrets in orphaned commits, dangling blobs, or branches you never checked out locally won't appear in the scan. If the Grok CLI uploaded the entire .git directory (including reflogs and packed refs), it captured more than grokpatrol detects. The tool correctly reports this limitation, but it means you can't treat 'CLEAN' as definitive proof of zero exposure—just proof that the specific artifacts grokpatrol knows to look for weren't found.
There's also the operational reality that logs rotate and upload queues drain. If you run grokpatrol two weeks after the incident and log rotation already purged ~/.grok/logs/, the tool can't distinguish 'Grok never ran here' from 'it ran but the evidence is gone'. The verdict becomes 'INDETERMINATE', which is honest but doesn't help with fleet-wide triage. You still need timeline correlation from other sources (package manager install logs, bash history, process accounting) to build confidence.
Verdict
Use if: You're responding to the specific Grok Build CLI incident and need to triage which developer machines were affected and what secrets need rotation—this tool answers that question better than any general-purpose scanner because it knows exactly what artifacts to look for. Also use it as a reference implementation if you're building forensic tools of your own; the structural security properties (no network, read-only, allowlisted subprocesses, degraded verdicts for blind spots) are a masterclass in trustworthy security tooling. Skip if: You never ran Grok Build CLI, need ongoing secret monitoring rather than one-time incident response, or want a general git secret scanner (use TruffleHog or Gitleaks instead). Also skip if you're on Windows or need a tool that adapts to other supply-chain incidents—grokpatrol is deliberately specific, not generalizable.