Back to skill

Security audit

Git Project Review and Snapshots

Security checks for vulnerabilities and agentic risk

Overview

This Git review tool needs review because inspecting a repository can run configured programs beyond its described read-only workflow.

Use repositories whose Git configuration you trust, or inspect untrusted repositories in a restricted environment. Keep snapshots private: includeUntracked:true captures all non-ignored untracked ordinary files. The concern is conditional program execution during inspection; the artifacts do not establish malicious intent.

Vulnerability Patterns
  • Skill Instruction HijackingAlters the agent's session goals or safety constraints when the skill loads
  • Agent Memory PoisoningWrites attacker-controlled rules into memory that affect later sessions
  • Remote Payload Retrieval and ExecutionFetches external code whose behavior can change after review
  • Embedded Malicious CodeShips malicious scripts inside the skill and executes them locally
  • Unauthorized Access and Privilege EscalationObtains permissions beyond the task's legitimate needs
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Output HandlingUnvalidated Output Injection, Cross-Context Output, Unbounded Output
  • Behavioral ASTexec() Call, eval() Call, Dynamic Import
  • Taint TrackingDirect Taint Flow, Variable-Mediated Taint Flow, Credential Exfiltration Chain
Findings (18)

Env Variable Harvesting

High
Category
Data Exfiltration
Confidence
70% confidence
Finding

Code enumerates, copies, or searches environment variables for secrets. Bulk environment access can collect credentials unrelated to the skill's stated purpose.

Content

Scanner excerpt · scripts/git_desk.py (reported line 5)May include surrounding context.

python
import os,subprocess,uuid,zipfile
from core import *
def git(repo,*args,limit=10*1024*1024):
    env={k:v for k,v in os.environ.items() if not k.startswith('GIT_')}; env['GIT_TERMINAL_PROMPT']='0'; env['GIT_OPTIONAL_LOCKS']='0'
    p=subprocess.run(['git','--no-optional-locks','-c','core.fsmonitor=false','-c','core.hooksPath=/dev/null','-C',str(repo),*args],capture_output=True,timeout=45,env=env)
    if p.returncode: raise ValueError('Git operation failed: '+args[0])
    if len(p.stdout)>limit: raise ValueError('Git output exceeds limit')

MCP server referenced without pinned version: 'npx clawhub'.

Medium
Category
MCP Rug Pull
Confidence
94% confidence
Finding

The recommended command uses npx clawhub@latest, which downloads and executes a mutable npm release rather than a reviewed, pinned version; this is a supply-chain risk, although the README does not identify an MCP server. A compromised future release could execute with the invoking user's permissions and access or modify private project files and credentials in the active workspace. The command is an optional installation step, not automatic execution, and there is no evidence of malicious intent.

Content

No source excerpt is available for this finding.

MCP server referenced without pinned version: 'npx clawhub'.

Medium
Category
MCP Rug Pull
Confidence
95% confidence
Finding

The documented command uses npx clawhub@latest, which downloads and executes a mutable package version rather than a reviewed, pinned release. A compromised or malicious future release could execute with the user's permissions and access local repositories and credentials; this is an installation supply-chain risk, although the finding incorrectly describes the CLI as an MCP server.

Content

No source excerpt is available for this finding.

Skill declares no tool scope ('permissions' or 'allowed-tools') but code capabilities were detected: env, file_read, file_write, shell.

Medium
Category
MCP Least Privilege
Confidence
70% confidence
Finding

Without declared permissions the skill's intent is opaque and cannot be validated.

Content

No source excerpt is available for this finding.

The atomic file writer silently replaces existing destination files without disclosing the overwrite behavior.

Medium
Category
Output Handling
Confidence
90% confidence
Finding

The atomic() helper writes data and uses os.replace() to overwrite any existing destination file. No confirmation, user-facing message, docstring, or comment explains that existing contents will be replaced, which can affect user data.

Content

No source excerpt is available for this finding.

Credential-environment access is unrelated to the declared local Git review and snapshot purpose.

Medium
Category
Excessive Agency
Confidence
90% confidence
Finding

The secret() helper reads a caller-selected environment variable and returns its value as a credential. Inspecting a selected Git project and creating private working snapshots does not require credential retrieval, and the manifest declares no authenticated remote operations. This file defines the capability but does not show it being invoked.

Content

No source excerpt is available for this finding.

subprocess module call

Medium
Category
Dangerous Code Execution
Confidence
93% confidence
Finding

The argument-list subprocess call prevents shell argument injection, but Git itself can execute configured helpers. If a selected repository contains attacker-controlled Git configuration defining a clean filter, matching .gitattributes entries can cause diff operations to execute that filter with the agent's privileges; disabling hooks, fsmonitor, external diffs, and textconv does not disable clean filters. This matters when reviewing untrusted local repositories, although ordinary tracked files alone cannot define the filter's executable command.

Content

Scanner excerpt · scripts/git_desk.py (reported line 6)May include surrounding context.

python
from core import *
def git(repo,*args,limit=10*1024*1024):
    env={k:v for k,v in os.environ.items() if not k.startswith('GIT_')}; env['GIT_TERMINAL_PROMPT']='0'; env['GIT_OPTIONAL_LOCKS']='0'
    p=subprocess.run(['git','--no-optional-locks','-c','core.fsmonitor=false','-c','core.hooksPath=/dev/null','-C',str(repo),*args],capture_output=True,timeout=45,env=env)
    if p.returncode: raise ValueError('Git operation failed: '+args[0])
    if len(p.stdout)>limit: raise ValueError('Git output exceeds limit')
    return p.stdout

subprocess module call

Medium
Category
Dangerous Code Execution
Confidence
70% confidence
Finding

subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Content

Scanner excerpt · tests/smoke.py (reported line 17)May include surrounding context.

python
class GitTests(Base):
    def setup_repo(self):
        repo=self.root/'repo'; repo.mkdir(); subprocess.run(['git','init','--quiet',str(repo)],check=True); subprocess.run(['git','-C',str(repo),'config','user.name','Test'],check=True); subprocess.run(['git','-C',str(repo),'config','user.email','test@example.com'],check=True); self.file('repo/a.txt','original\n'); subprocess.run(['git','-C',str(repo),'add','a.txt'],check=True); subprocess.run(['git','-C',str(repo),'commit','--quiet','-m','Initial'],check=True); return repo
    def test_real_status_history(self):
        self.setup_repo(); self.file('repo/a.txt','changed\n'); s=gd.handle(self.st,{'action':'status','repository':'repo'}); self.assertEqual(s['changes'][0]['path'],'a.txt'); h=gd.handle(self.st,{'action':'history','repository':'repo'}); self.assertEqual(h[0]['subject'],'Initial')
    def test_real_checkpoint(self):

subprocess module call

Medium
Category
Dangerous Code Execution
Confidence
70% confidence
Finding

subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Content

Scanner excerpt · tests/smoke.py (reported line 17)May include surrounding context.

python
class GitTests(Base):
    def setup_repo(self):
        repo=self.root/'repo'; repo.mkdir(); subprocess.run(['git','init','--quiet',str(repo)],check=True); subprocess.run(['git','-C',str(repo),'config','user.name','Test'],check=True); subprocess.run(['git','-C',str(repo),'config','user.email','test@example.com'],check=True); self.file('repo/a.txt','original\n'); subprocess.run(['git','-C',str(repo),'add','a.txt'],check=True); subprocess.run(['git','-C',str(repo),'commit','--quiet','-m','Initial'],check=True); return repo
    def test_real_status_history(self):
        self.setup_repo(); self.file('repo/a.txt','changed\n'); s=gd.handle(self.st,{'action':'status','repository':'repo'}); self.assertEqual(s['changes'][0]['path'],'a.txt'); h=gd.handle(self.st,{'action':'history','repository':'repo'}); self.assertEqual(h[0]['subject'],'Initial')
    def test_real_checkpoint(self):

subprocess module call

Medium
Category
Dangerous Code Execution
Confidence
70% confidence
Finding

subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Content

Scanner excerpt · tests/smoke.py (reported line 17)May include surrounding context.

python
class GitTests(Base):
    def setup_repo(self):
        repo=self.root/'repo'; repo.mkdir(); subprocess.run(['git','init','--quiet',str(repo)],check=True); subprocess.run(['git','-C',str(repo),'config','user.name','Test'],check=True); subprocess.run(['git','-C',str(repo),'config','user.email','test@example.com'],check=True); self.file('repo/a.txt','original\n'); subprocess.run(['git','-C',str(repo),'add','a.txt'],check=True); subprocess.run(['git','-C',str(repo),'commit','--quiet','-m','Initial'],check=True); return repo
    def test_real_status_history(self):
        self.setup_repo(); self.file('repo/a.txt','changed\n'); s=gd.handle(self.st,{'action':'status','repository':'repo'}); self.assertEqual(s['changes'][0]['path'],'a.txt'); h=gd.handle(self.st,{'action':'history','repository':'repo'}); self.assertEqual(h[0]['subject'],'Initial')
    def test_real_checkpoint(self):

subprocess module call

Medium
Category
Dangerous Code Execution
Confidence
70% confidence
Finding

subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Content

Scanner excerpt · tests/smoke.py (reported line 17)May include surrounding context.

python
class GitTests(Base):
    def setup_repo(self):
        repo=self.root/'repo'; repo.mkdir(); subprocess.run(['git','init','--quiet',str(repo)],check=True); subprocess.run(['git','-C',str(repo),'config','user.name','Test'],check=True); subprocess.run(['git','-C',str(repo),'config','user.email','test@example.com'],check=True); self.file('repo/a.txt','original\n'); subprocess.run(['git','-C',str(repo),'add','a.txt'],check=True); subprocess.run(['git','-C',str(repo),'commit','--quiet','-m','Initial'],check=True); return repo
    def test_real_status_history(self):
        self.setup_repo(); self.file('repo/a.txt','changed\n'); s=gd.handle(self.st,{'action':'status','repository':'repo'}); self.assertEqual(s['changes'][0]['path'],'a.txt'); h=gd.handle(self.st,{'action':'history','repository':'repo'}); self.assertEqual(h[0]['subject'],'Initial')
    def test_real_checkpoint(self):

subprocess module call

Medium
Category
Dangerous Code Execution
Confidence
70% confidence
Finding

subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Content

Scanner excerpt · tests/smoke.py (reported line 17)May include surrounding context.

python
class GitTests(Base):
    def setup_repo(self):
        repo=self.root/'repo'; repo.mkdir(); subprocess.run(['git','init','--quiet',str(repo)],check=True); subprocess.run(['git','-C',str(repo),'config','user.name','Test'],check=True); subprocess.run(['git','-C',str(repo),'config','user.email','test@example.com'],check=True); self.file('repo/a.txt','original\n'); subprocess.run(['git','-C',str(repo),'add','a.txt'],check=True); subprocess.run(['git','-C',str(repo),'commit','--quiet','-m','Initial'],check=True); return repo
    def test_real_status_history(self):
        self.setup_repo(); self.file('repo/a.txt','changed\n'); s=gd.handle(self.st,{'action':'status','repository':'repo'}); self.assertEqual(s['changes'][0]['path'],'a.txt'); h=gd.handle(self.st,{'action':'history','repository':'repo'}); self.assertEqual(h[0]['subject'],'Initial')
    def test_real_checkpoint(self):

Tainted flow: 'repo' from os.environ.get (line 17, credential/environment) → subprocess.run (code execution)

Medium
Category
Data Flow
Confidence
65% confidence
Finding

Data from a source is assigned to a variable that is later passed to a sink, creating a variable-mediated taint flow.

Content

Scanner excerpt · tests/smoke.py (reported line 17)May include surrounding context.

python
class GitTests(Base):
    def setup_repo(self):
        repo=self.root/'repo'; repo.mkdir(); subprocess.run(['git','init','--quiet',str(repo)],check=True); subprocess.run(['git','-C',str(repo),'config','user.name','Test'],check=True); subprocess.run(['git','-C',str(repo),'config','user.email','test@example.com'],check=True); self.file('repo/a.txt','original\n'); subprocess.run(['git','-C',str(repo),'add','a.txt'],check=True); subprocess.run(['git','-C',str(repo),'commit','--quiet','-m','Initial'],check=True); return repo
    def test_real_status_history(self):
        self.setup_repo(); self.file('repo/a.txt','changed\n'); s=gd.handle(self.st,{'action':'status','repository':'repo'}); self.assertEqual(s['changes'][0]['path'],'a.txt'); h=gd.handle(self.st,{'action':'history','repository':'repo'}); self.assertEqual(h[0]['subject'],'Initial')
    def test_real_checkpoint(self):

Tainted flow: 'repo' from os.environ.get (line 17, credential/environment) → subprocess.run (code execution)

Medium
Category
Data Flow
Confidence
65% confidence
Finding

Data from a source is assigned to a variable that is later passed to a sink, creating a variable-mediated taint flow.

Content

Scanner excerpt · tests/smoke.py (reported line 17)May include surrounding context.

python
class GitTests(Base):
    def setup_repo(self):
        repo=self.root/'repo'; repo.mkdir(); subprocess.run(['git','init','--quiet',str(repo)],check=True); subprocess.run(['git','-C',str(repo),'config','user.name','Test'],check=True); subprocess.run(['git','-C',str(repo),'config','user.email','test@example.com'],check=True); self.file('repo/a.txt','original\n'); subprocess.run(['git','-C',str(repo),'add','a.txt'],check=True); subprocess.run(['git','-C',str(repo),'commit','--quiet','-m','Initial'],check=True); return repo
    def test_real_status_history(self):
        self.setup_repo(); self.file('repo/a.txt','changed\n'); s=gd.handle(self.st,{'action':'status','repository':'repo'}); self.assertEqual(s['changes'][0]['path'],'a.txt'); h=gd.handle(self.st,{'action':'history','repository':'repo'}); self.assertEqual(h[0]['subject'],'Initial')
    def test_real_checkpoint(self):

Tainted flow: 'repo' from os.environ.get (line 17, credential/environment) → subprocess.run (code execution)

Medium
Category
Data Flow
Confidence
65% confidence
Finding

Data from a source is assigned to a variable that is later passed to a sink, creating a variable-mediated taint flow.

Content

Scanner excerpt · tests/smoke.py (reported line 17)May include surrounding context.

python
class GitTests(Base):
    def setup_repo(self):
        repo=self.root/'repo'; repo.mkdir(); subprocess.run(['git','init','--quiet',str(repo)],check=True); subprocess.run(['git','-C',str(repo),'config','user.name','Test'],check=True); subprocess.run(['git','-C',str(repo),'config','user.email','test@example.com'],check=True); self.file('repo/a.txt','original\n'); subprocess.run(['git','-C',str(repo),'add','a.txt'],check=True); subprocess.run(['git','-C',str(repo),'commit','--quiet','-m','Initial'],check=True); return repo
    def test_real_status_history(self):
        self.setup_repo(); self.file('repo/a.txt','changed\n'); s=gd.handle(self.st,{'action':'status','repository':'repo'}); self.assertEqual(s['changes'][0]['path'],'a.txt'); h=gd.handle(self.st,{'action':'history','repository':'repo'}); self.assertEqual(h[0]['subject'],'Initial')
    def test_real_checkpoint(self):

Tainted flow: 'repo' from os.environ.get (line 17, credential/environment) → subprocess.run (code execution)

Medium
Category
Data Flow
Confidence
65% confidence
Finding

Data from a source is assigned to a variable that is later passed to a sink, creating a variable-mediated taint flow.

Content

Scanner excerpt · tests/smoke.py (reported line 17)May include surrounding context.

python
class GitTests(Base):
    def setup_repo(self):
        repo=self.root/'repo'; repo.mkdir(); subprocess.run(['git','init','--quiet',str(repo)],check=True); subprocess.run(['git','-C',str(repo),'config','user.name','Test'],check=True); subprocess.run(['git','-C',str(repo),'config','user.email','test@example.com'],check=True); self.file('repo/a.txt','original\n'); subprocess.run(['git','-C',str(repo),'add','a.txt'],check=True); subprocess.run(['git','-C',str(repo),'commit','--quiet','-m','Initial'],check=True); return repo
    def test_real_status_history(self):
        self.setup_repo(); self.file('repo/a.txt','changed\n'); s=gd.handle(self.st,{'action':'status','repository':'repo'}); self.assertEqual(s['changes'][0]['path'],'a.txt'); h=gd.handle(self.st,{'action':'history','repository':'repo'}); self.assertEqual(h[0]['subject'],'Initial')
    def test_real_checkpoint(self):

Tainted flow: 'repo' from os.environ.get (line 17, credential/environment) → subprocess.run (code execution)

Medium
Category
Data Flow
Confidence
65% confidence
Finding

Data from a source is assigned to a variable that is later passed to a sink, creating a variable-mediated taint flow.

Content

Scanner excerpt · tests/smoke.py (reported line 17)May include surrounding context.

python
class GitTests(Base):
    def setup_repo(self):
        repo=self.root/'repo'; repo.mkdir(); subprocess.run(['git','init','--quiet',str(repo)],check=True); subprocess.run(['git','-C',str(repo),'config','user.name','Test'],check=True); subprocess.run(['git','-C',str(repo),'config','user.email','test@example.com'],check=True); self.file('repo/a.txt','original\n'); subprocess.run(['git','-C',str(repo),'add','a.txt'],check=True); subprocess.run(['git','-C',str(repo),'commit','--quiet','-m','Initial'],check=True); return repo
    def test_real_status_history(self):
        self.setup_repo(); self.file('repo/a.txt','changed\n'); s=gd.handle(self.st,{'action':'status','repository':'repo'}); self.assertEqual(s['changes'][0]['path'],'a.txt'); h=gd.handle(self.st,{'action':'history','repository':'repo'}); self.assertEqual(h[0]['subject'],'Initial')
    def test_real_checkpoint(self):

Scope Creep

Low
Category
Excessive Agency
Confidence
70% confidence
Finding

Skill's behavior or capabilities extend beyond its stated purpose. Scope creep allows an agent to perform actions unrelated to its documented functionality, increasing the attack surface.

Content

Scanner excerpt · LICENSE (reported line 7)May include surrounding context.

text
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

Static analysis

No suspicious patterns detected.