{"slug": "my-read-only-python-scanner-accidentally-ran-a-git-helper", "title": "My read-only Python scanner accidentally ran a Git helper", "summary": "A developer building ClonePeek, a read-only Python scanner that audits repository configuration without executing project code, found that its optional --changed-since flag could trigger repository-defined execution: a local core.fsmonitor setting can cause Git to launch a hook-like helper during a plain git diff. The team reproduced the violation with a disposable fixture whose inert helper wrote a marker file, then hardened the invocation by disabling fsmonitor, hooks, external diff and textconv, restricting protocols, stripping inherited Git and loader environment variables, and bounding subprocess runtime. The fix, documented in PR #5 and Git configuration isolation notes, is framed as defense-in-depth for one limited operation rather than process isolation.", "body_md": "We were building a tool whose defining promise was simple: inspect unfamiliar repository configuration without executing its code. Then a test revealed that an ordinary-looking Git operation could launch a program defined by the repository itself.\n\nYou're about to open a GitHub repository in your editor. You want to know which commands may run through package installation, trusted workspace tasks, devcontainer startup, MCP servers or an AI coding agent. A static auditor looks like a reasonable first line of review.\n\n[ClonePeek](https://github.com/harshitethic/clonepeek) was designed for that job. Its scanner traverses recognized configuration files using bounded reads and reports **trigger → command → source location → suggested review** , without launching the project's npm scripts, shell hooks, or coding agents.\n\nThere is a useful optional flag: `--changed-since` filters findings to Git changes. Our first implementation called a local `git diff` command. It felt harmless. Diffing files isn't building the repository, right?\n\nThe catch is that **Git's execution behavior can depend on checkout-local or inherited configuration, not just the arguments my code passes**. In particular, a local `core.fsmonitor` setting can name a hook-like helper program. Depending on the command and Git version, Git may invoke that helper while refreshing file state. A subprocess invoked by the auditor was therefore enough to break our passive-scan invariant.\n\nOne important boundary: a normal `git clone` does **not** transfer the remote repo's `.git/config`. The issue is an existing checkout and its local configuration or inherited Git environment; don't confuse it with a malicious `package.json` remotely rewriting Git config simply by being cloned.\n\nTo test this safely, we created a disposable local fixture with a deliberately inert helper that merely wrote a marker file. We did not use a real suspicious repository, download an executable payload, or depend on a network request.\n\nThe test asked one specific question: **can the audit complete without creating that marker?** Before the fix, the marker appeared. After the fix, it did not. Reproducing the violation as a filesystem side effect made it much harder for later refactors to accidentally reintroduce it.\n\nThis is not a claim that every `git diff` invocation on every operating system will execute an fsmonitor helper. It's a concrete example of why \"I only ran Git\" doesn't establish that no repo-defined execution occurred.\n\nThe exact reproduction and safety constraints are in the [Git configuration isolation notes](https://github.com/harshitethic/clonepeek/blob/main/docs/GIT-CONFIG-ISOLATION.md); the patch and review trail are in [PR #5](https://github.com/harshitethic/clonepeek/pull/5).\n\nPython's read-only filesystem operations were not the issue. The boundary changed when the scanner delegated work to Git, a separate executable capable of honoring configuration, helpers and extensions. Fixing it required controlling *both* process arguments and the environment.\n\n```\n# Excerpted structure, not a standalone security sandbox\nargv = [\n    \"git\",\n    \"-c\", \"core.fsmonitor=false\",\n    \"-c\", f\"core.hooksPath={os.devnull}\",\n    \"-c\", \"diff.external=\",\n    \"-c\", \"protocol.ext.allow=never\",\n    \"-c\", \"protocol.file.allow=never\",\n    \"-C\", str(repo_root),\n    \"diff\", \"--no-ext-diff\", \"--no-textconv\",\n    \"--name-only\", \"-z\", \"--diff-filter=ACMR\",\n    reference, \"--\",\n]\n```\n\nThe scanner also strips inherited Git-specific variables and dangerous loader/Python environment overrides, disables system/global Git config for this invocation, rejects unexpected ref syntax, bounds subprocess runtime, and treats a failed diff as an error rather than silently reporting an empty result. These are **defense-in-depth choices for an explicitly limited operation** ; they are not equivalent to process isolation.\n\n**Important limit:** This hardening is targeted to the Git invocation used by ClonePeek. Git and the host operating system have more behavior than one wrapper can model. Do not treat ClonePeek as a sandbox, a malware detector, or a reliable way to execute other tools against actively malicious content.\n\nAnother coverage gap turned out to be less dramatic but equally useful. A `package.json` file could contain a short lifecycle command such as `sh ./scripts/setup.sh`. A surface-only scanner recognized the install hook, but the interesting command might live inside `scripts/setup.sh`, outside the JSON.\n\nWe added **bounded, first-hop, read-only inspection** for statically identifiable local `.sh` references. It checks that paths stay inside the repository, rejects symlink escapes and unsafe file types, and limits references, file sizes and lines read. It doesn't shell-expand arbitrary expressions or chase unbounded nested scripts.\n\nThat sounds modest—and that's intentional. A scanner that executes the referenced script \"to find out what it does\" would solve one detection problem by discarding its own execution boundary. ClonePeek instead tells you what it can and can't see.\n\nInternal tests can miss assumptions about packaging. The v0.5.0 GitHub Action therefore has separate integration CI on Linux, macOS and Windows, plus an actual consumer workflow in another repository. That consumer workflow read two pinned, public configuration snapshots (Cursor hooks and VS Code folder-open tasks), checked their Git blob hashes, and asserted paths and classifications. No upstream hooks were run.\n\nThese are narrow [smoke tests](https://github.com/harshitethic/clonepeek/blob/main/docs/CROSS-REPO-SMOKE.md), not independent outside-user adoption or an estimate of detection accuracy. The [hand-authored synthetic evaluation](https://github.com/harshitethic/clonepeek/blob/main/docs/EVALUATION.md) deliberately includes false positives and false negatives; it is not representative of internet repositories.\n\nIf you're auditing a repository you don't trust, the safest workflow begins before installing its dependencies or marking it trusted in an IDE. The [five-minute pre-trust audit guide](https://github.com/harshitethic/clonepeek/blob/main/docs/BEFORE-YOU-TRUST-A-REPO.md) covers practical next steps.\n\nClonePeek v0.5.1 is MIT licensed, has no runtime Python dependencies, and exposes its threat model publicly. We're looking for real, inert examples of execution hooks it misses or overclassifies—not drive-by stars or artificial PRs.\n\n```\nuvx --from git+https://github.com/harshitethic/clonepeek@v0.5.1 \\\n  clonepeek /path/to/repository\n```\n\n[Browse source ↗](https://github.com/harshitethic/clonepeek) [Contribute a real improvement ↗](https://github.com/harshitethic/clonepeek/issues/14) [Live demo ↗](https://harshitethic.github.io/clonepeek/)\n\n*Disclosure: I maintain ClonePeek. This case study was prepared with AI writing assistance and reviewed against the source, tests, and public docs. Maintainer-run tests are not independent security evaluation.*", "url": "https://wpnews.pro/news/my-read-only-python-scanner-accidentally-ran-a-git-helper", "canonical_source": "https://dev.to/harshitethic/my-read-only-python-scanner-accidentally-ran-a-git-helper-2ni0", "published_at": "2026-10-17 04:21:42+00:00", "updated_at": "2026-10-17 04:30:18.588943+00:00", "lang": "en", "topics": ["ai-tools", "ai-agents", "developer-tools", "ai-safety"], "entities": ["ClonePeek", "Git", "GitHub", "Python"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/my-read-only-python-scanner-accidentally-ran-a-git-helper", "markdown": "https://wpnews.pro/news/my-read-only-python-scanner-accidentally-ran-a-git-helper.md", "text": "https://wpnews.pro/news/my-read-only-python-scanner-accidentally-ran-a-git-helper.txt", "jsonld": "https://wpnews.pro/news/my-read-only-python-scanner-accidentally-ran-a-git-helper.jsonld"}}