Back to skill

Security audit

Prompt Git

Security checks for vulnerabilities and agentic risk

Overview

PromptGit is a coherent local prompt manager, but it has a real local file-read safety issue when repository metadata is poisoned.

Review before installing if you may use shared, synced, or imported PromptGit repositories. Keep the repository private, do not import or sync untrusted repository metadata, back up before using --overwrite or manual deletion, and avoid storing secrets or regulated data unless filesystem permissions or encryption protect the files.

Vulnerability Patterns
  • Insecure Skill Coding PracticesFinds exploitable flaws such as hardcoded secrets or command injection
  • 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
Findings (2)

T09 · Insecure Skill Coding Practices

Warning
Location
prompt_git.py:127
Finding
Unvalidated version identifiers permit path traversal during content reads<![CDATA[ ## Vulnerability Details **File Location**: `prompt_git.py:127-142` and `prompt_git.py:263-270` **Vulnerability Type**: Path traversal through repository metadata **Risk Level**: Medium ### Vulnerable Code ```python def _get_content_path(self, name: str, version_id: str) -> str: """Get content file path for a version.""" return os.path.join(self._get_prompt_dir(name), f'{version_id}.txt') def _load_versions(self, name: str) -> List[PromptVersion]: """Load version history for a prompt.""" versions_path = self._get_versions_path(name) if not os.path.exists(versions_path): return [] try: with open(versions_path, 'r', encoding='utf-8') as f: data = json.load(f) return [PromptVersion(**v) for v in data] ``` The resulting path is subsequently opened without verifying that it remains inside the prompt directory: ```python for v in versions: if v.id == version_id: # Load content content_path = self._get_content_path(name, v.id) try: with open(content_path, 'r', encoding='utf-8') as f: v.content = f.read() return v except FileNotFoundError: return None ``` ### Technical Analysis Prompt names are validated by `_sanitize_name()`, but version identifiers loaded from `versions.json` are trusted without equivalent validation. `_get_content_path()` directly appends the metadata-controlled identifier to a filesystem path. A version identifier such as `../../target` produces a path ending in `../../target.txt`. The operating system normalizes these traversal components when the file is opened, allowing the read operation to escape the intended prompt directory. The generated version identifiers are normally 16-character hexadecimal SHA-256 prefixes, so accepting path separators or identifiers outside that format is unnecessary. This issue is especially relevant to repositories obtained through the documented G ...[truncated 1412 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Validate every version identifier before using it as a path component. Since identifiers are generated as 16-character hexadecimal hashes, enforce that exact format: ```python import re def _validate_version_id(self, version_id: str) -> str: if not isinstance(version_id, str) or not re.fullmatch(r'[0-9a-f]{16}', version_id): raise ValueError("Invalid version identifier") return version_id ``` 2. Perform containment validation on the complete resolved file path: ```python def _get_content_path(self, name: str, version_id: str) -> str: self._validate_version_id(version_id) prompt_dir = os.path.realpath(self._get_prompt_dir(name)) candidate = os.path.realpath( os.path.join(prompt_dir, f'{version_id}.txt') ) if os.path.commonpath([prompt_dir, candidate]) != prompt_dir: raise ValueError("Content path escapes the prompt directory") return candidate ``` 3. Validate all fields loaded from `versions.json` and `index.json` against an explicit schema before constructing `PromptVersion` objects. 4. Reject inconsistent metadata where a current version is absent from the validated version history. 5. Add regression tests covering `../`, `..\`, absolute paths, null bytes, malformed hashes, and poisoned `index.json` or `versions.json` files. ]]>

T09 · Insecure Skill Coding Practices

Note
Location
prompt_git.py:72
Finding
Prompt repository files are created without enforced private permissions<![CDATA[ ## Vulnerability Details **File Location**: `prompt_git.py:72-75`, `prompt_git.py:88-91`, `prompt_git.py:147-154`, and `prompt_git.py:203-206` **Vulnerability Type**: Insecure filesystem permissions for potentially sensitive prompt data **Risk Level**: Low ### Vulnerable Code Repository directories are created using permissions derived entirely from the process umask: ```python def _ensure_storage(self): """Ensure storage directories exist.""" os.makedirs(self.storage_dir, exist_ok=True) os.makedirs(self.prompts_dir, exist_ok=True) if not os.path.exists(self.index_path): self._save_index({}) ``` Metadata and prompt content are similarly created without explicit restrictive modes: ```python def _save_index(self, index: Dict[str, Dict]): """Save the prompt index.""" with open(self.index_path, 'w', encoding='utf-8') as f: json.dump(index, f, indent=2) ``` ```python def _save_versions(self, name: str, versions: List[PromptVersion]): """Save version history for a prompt.""" prompt_dir = self._get_prompt_dir(name) os.makedirs(prompt_dir, exist_ok=True) versions_path = self._get_versions_path(name) with open(versions_path, 'w', encoding='utf-8') as f: json.dump([asdict(v) for v in versions], f, indent=2) ``` ```python # Save content file content_path = self._get_content_path(name, version_id) with open(content_path, 'w', encoding='utf-8') as f: f.write(content) ``` ### Technical Analysis PromptGit stores prompt contents and metadata as plaintext. Directory and file permissions are inherited from the caller's environment rather than being explicitly restricted. On a multi-user system with a permissive umask, repository directories may be traversable and files may be readable by other local accounts. Prompt contents can include proprietary system prompts, internal operational instructions, or data copied into prompts by users. The issue does not bypass operating-system permi ...[truncated 1054 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Create repository and prompt directories with owner-only permissions: ```python os.makedirs(self.storage_dir, mode=0o700, exist_ok=True) os.makedirs(self.prompts_dir, mode=0o700, exist_ok=True) ``` 2. Use low-level file creation with mode `0600` for new files, or explicitly apply owner-only permissions after safe creation: ```python fd = os.open( content_path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600 ) with os.fdopen(fd, 'w', encoding='utf-8') as f: f.write(content) ``` 3. Apply the same protection to `index.json`, `versions.json`, exported JSON files, and Markdown exports where confidentiality is expected. 4. Inspect existing repository permissions during initialization and warn users if directories or files are readable by group or other users. Avoid silently changing permissions when shared-repository behavior is intentional. 5. Document that prompts are stored in plaintext and should not contain credentials or regulated information unless the repository is protected by filesystem permissions or encrypted storage. 6. Consider an optional encrypted-at-rest storage mode for repositories intended to contain confidential prompts. ]]>
Vulnerability Patterns
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • System Prompt LeakageDirect Leakage, Indirect Extraction, Tool-Based Exfiltration
  • Rogue AgentSelf-Modification, Session Persistence
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
Findings (20)

Hidden Instructions

High
Category
Prompt Injection
Content
# PromptGit — Local Prompt Version Control

**Git for your prompts. Track every change, diff versions, rollback mistakes, never lose a good prompt again.**
Confidence
60% confidence
Finding
Hidden instructions were detected in comments or invisible text. These could contain malicious directives. Manual review is recommended.

Tp4

High
Category
MCP Tool Poisoning
Confidence
93% confidence
Finding
The code is related to prompt management and remains local/offline, so it is within the same general domain. However, the declared description emphasizes Git-like version tracking features such as tracking changes, diffing versions, and rollback. This specific code chunk does not implement diffing or rollback; instead, its primary function is importing and exporting prompts and histories for sharing. That is a materially different capability not reflected in the declared description. Although export/import may be a supporting feature of a broader prompt versioning tool, for this code chunk the main behavior is undeclared, so this should be flagged as a mismatch.

Tp4

High
Category
MCP Tool Poisoning
Confidence
92% confidence
Finding
The supplied code chunk does not implement the declared core functionality of version tracking, diffing versions, or rollback. Instead, it provides a search/browse CLI for an existing prompt repository. While this may be part of a broader prompt-management tool, this specific chunk’s primary behavior is materially different from the declared purpose. It remains local/offline in spirit and does not introduce suspicious external access, but the functional description is incomplete/inaccurate for this code because it omits the search, filtering, similarity, and stats capabilities and emphasizes version-control operations not present here.

Direct Prompt Extraction

High
Category
System Prompt Leakage
Content
reverse=True
        )
        
        return prompts[:limit]
    
    def get_similar(self, name: str, threshold: float = 0.5) -> List[tuple]:
        """
Confidence
85% confidence
Finding
Skill contains instructions that could directly expose system prompts, internal rules, or hidden instructions to users or external parties.

Direct Prompt Extraction

High
Category
System Prompt Leakage
Content
reverse=True
        )
        
        return prompts[:limit]
    
    def get_similar(self, name: str, threshold: float = 0.5) -> List[tuple]:
        """
Confidence
85% confidence
Finding
Skill contains instructions that could directly expose system prompts, internal rules, or hidden instructions to users or external parties.

Missing User Warnings

Medium
Confidence
83% confidence
Finding
This markdown file instructs users to manually delete folders from `~/.promptgit/prompts/`, which is a destructive operation affecting stored prompt data. While the section explains that deletion is not built in to avoid accidental deletes, the workaround itself does not explicitly warn that manual folder deletion can permanently remove prompts or versions if they are not otherwise backed up.

Session Persistence

Medium
Category
Rogue Agent
Content
# Import a prompt
python3 prompt_export.py import shared-prompt.json

# Import and overwrite existing
python3 prompt_export.py import shared-prompt.json --overwrite
```
Confidence
60% confidence
Finding
Skill establishes unauthorized persistence across sessions via cron jobs, startup scripts, or state files. Session persistence allows an attacker to maintain access beyond the current interaction.

Lp3

Medium
Category
MCP Least Privilege
Confidence
70% confidence
Finding
Without declared permissions the skill's intent is opaque and cannot be validated.

Missing User Warnings

Medium
Confidence
92% confidence
Finding
The documented `import ... --overwrite` flow can replace existing prompt history or current prompt state without a prominently colocated warning, increasing the risk of accidental destructive writes. In a tool that manages long-lived prompt assets, users may invoke overwrite casually and lose important local data or replace trusted prompts with imported content.

Session Persistence

Medium
Category
Rogue Agent
Content
# Import a prompt
python3 prompt_export.py import shared-prompt.json

# Import and overwrite existing
python3 prompt_export.py import shared-prompt.json --overwrite
```
Confidence
77% confidence
Finding
The documented overwrite import operation can persist externally sourced content into the local repository and replace existing records, creating a durable state change from imported data. Even though the README advises against importing untrusted JSON elsewhere, the usage example normalizes overwrite behavior without emphasizing persistence and recovery risks at the point of action.

Missing User Warnings

Medium
Confidence
87% confidence
Finding
Telling users to manually delete the prompt folder without an adjacent irreversible-data-loss warning creates a realistic risk of accidental destruction of stored prompts and version history. Because the storage layout is filesystem-based, users may remove the wrong directory or assume recovery is supported when it is not.

Session Persistence

Medium
Category
Rogue Agent
Content
# Check if exists
        existing = self.repo.get_version(name)
        if existing and not overwrite:
            raise ValueError(f"Prompt already exists: {name}. Use --overwrite to replace.")
        
        # Import history if available and requested
        if preserve_history and 'history' in data:
Confidence
60% confidence
Finding
Skill establishes unauthorized persistence across sessions via cron jobs, startup scripts, or state files. Session persistence allows an attacker to maintain access beyond the current interaction.

Missing User Warnings

Medium
Confidence
94% confidence
Finding
When importing with both overwrite enabled and history preservation, the code deletes the existing versions file before reconstructing history from untrusted import data. If the import is malformed, incomplete, reordered, or fails partway through, prior local history can be permanently lost without an additional confirmation step or atomic rollback path.

Session Persistence

Medium
Category
Rogue Agent
Content
# Get parent (most recent version)
        parent_id = versions[-1].id if versions else None
        
        # Create new version
        version = PromptVersion(
            id=version_id,
            content=content,
Confidence
60% confidence
Finding
Skill establishes unauthorized persistence across sessions via cron jobs, startup scripts, or state files. Session persistence allows an attacker to maintain access beyond the current interaction.

Missing User Warnings

Low
Confidence
86% confidence
Finding
This markdown file includes an explicit example for `import shared-prompt.json --overwrite`, which indicates a potentially destructive operation that can replace existing stored prompt content. Although the README contains a general disclaimer later, it does not provide a specific user warning near this operation about overwriting existing data or recommending backup/review before use.

Scope Creep

Low
Category
Excessive Agency
Content
**USE AT YOUR OWN RISK.**

- The author(s) are NOT liable for any damages, losses, or consequences arising from 
  the use or misuse of this software — including but not limited to financial loss, 
  data loss, security breaches, business interruption, or any indirect/consequential damages.
- This software does NOT constitute financial, legal, trading, or professional advice.
- Users are solely responsible for evaluating whether this software is suitable for
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.

Scope Creep

Low
Category
Excessive Agency
Content
**USE AT YOUR OWN RISK.**

- The author(s) are NOT liable for any damages, losses, or consequences arising from 
  the use or misuse of this software — including but not limited to financial loss, 
  data loss, security breaches, business interruption, or any indirect/consequential damages.
- This software does NOT constitute financial, legal, trading, or professional advice.
- Users are solely responsible for evaluating whether this software is suitable for
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.

Vague Triggers

Low
Confidence
82% confidence
Finding
The manifest description is broad marketing language about tracking and rolling back prompts, but it does not define when the skill should activate or what user requests should map to this skill versus other prompt-editing tasks. For manifest/markdown files, the absence of specific trigger scope or negative examples can cause unintended invocations in systems that rely on descriptive matching.

Dynamic attribute access via getattr()

Low
Category
Dangerous Code Execution
Content
results = []
        
        for prompt in prompts:
            date_str = getattr(prompt, field, None)
            if not date_str:
                continue
Confidence
50% confidence
Finding
Dynamic getattr() with a non-literal attribute name can access arbitrary object attributes, potentially bypassing access controls.

Dynamic attribute access via getattr()

Low
Category
Dangerous Code Execution
Content
else:
                print(f"Found {len(results)} prompts:\n")
                for p in results:
                    date_val = getattr(p, args.field)
                    print(f"  {p.name} — {date_val[:10]}")
        
        elif args.command == 'regex':
Confidence
50% confidence
Finding
Dynamic getattr() with a non-literal attribute name can access arbitrary object attributes, potentially bypassing access controls.

Static analysis

No suspicious patterns detected.