Skill: amascia-gg/ggshield-scanner (version 1.0.2)

--- name: ggshield-scanner description: Detect 500+ types of hardcoded secrets (API keys, credentials, tokens) before they leak into git. Wraps GitGuardian's ggshield CLI. homepage: https://github.com/GitGuardian/ggshield-skill metadata: clawdbot: requires: bins: ["ggshield"] env: ["GITGUARDIAN_API_KEY"] --- # ggshield Secret Scanner ## Overview **ggshield** is a CLI tool that detects hardcoded secrets in your codebase. This Moltbot skill brings secret scanning capabilities to your AI agent. ### What Are "Secrets"? Secrets are sensitive credentials that should NEVER be committed to version control: - AWS Access Keys, GCP Service Accounts, Azure credentials - API tokens (GitHub, Slack, Stripe, etc.) - Database passwords and connection strings - Private encryption keys and certificates - OAuth tokens and refresh tokens - PayPal/Stripe API keys - Email server credentials ### Why This Matters A single leaked secret can: - 🔓 Compromise your infrastructure - 💸 Incur massive cloud bills (attackers abuse your AWS account) - 📊 Expose customer data (GDPR/CCPA violation) - 🚨 Trigger security incidents and audits ggshield catches these **before** they reach your repository. ## Features ### Commands Available #### 1. `scan-repo` Scans an entire git repository for secrets (including history). ``` @clawd scan-repo /path/to/my/project ``` **Output**: ``` 🔍 Scanning repository... ✅ Repository clean: 1,234 files scanned, 0 secrets found ``` **Output on detection**: ``` ❌ Found 2 secrets: - AWS Access Key ID in config/prod.py:42 - Slack API token in .env.backup:8 Use 'ggshield secret ignore --last-found' to ignore, or remove them. ``` #### 2. `scan-file` Scans a single file for secrets. ``` @clawd scan-file /path/to/config.py ``` #### 3. `scan-staged` Scans only staged git changes (useful pre-commit check). ``` @clawd scan-staged ``` This runs on your `git add`-ed changes only (fast!). #### 4. `install-hooks` Installs ggshield as a git pre-commit hook. ``` @clawd install-hooks ``` After this, every commit is automatically scanned: ``` $ git commit -m "Add config" 🔍 Running ggshield pre-commit hook... ❌ Secrets detected! Commit blocked. Remove the secrets and try again. ``` #### 5. `scan-docker` Scans Docker images for secrets in their layers. ``` @clawd scan-docker my-app:latest ``` ## Installation ### Prerequisites 1. **ggshield CLI**: Install via pip ```bash pip install ggshield>=1.15.0 ``` 2. **GitGuardian API Key**: Required for secret detection - Sign up: https://dashboard.gitguardian.com (free) - Generate API key in Settings - Set environment variable: ```bash export GITGUARDIAN_API_KEY="your-api-key-here" ``` 3. **Python 3.8+**: Required by ggshield ### Install Skill ```bash clawdhub install ggshield-scanner ``` The skill is now available in your Moltbot workspace. ### In Your Moltbot Workspace Start a new Moltbot session to pick up the skill: ```bash moltbot start # or via messaging: @clawd list-skills ``` ## Usage Patterns ### Pattern 1: Before Pushing (Security Check) ``` Dev: @clawd scan-repo . Moltbot: ✅ Repository clean. All good to push! Dev: git push ``` ### Pattern 2: Audit Existing Repo ``` Dev: @clawd scan-repo ~/my-old-project Moltbot: ❌ Found 5 secrets in history! - AWS keys in config/secrets.json - Database password in docker-compose.yml - Slack webhook in .env.example Moltbot: Recommendation: Rotate these credentials immediately. Consider using git-filter-repo to remove from history. ``` ### Pattern 3: Pre-Commit Enforcement ``` Dev: @clawd install-hooks Moltbot: ✅ Installed pre-commit hook Dev: echo "SECRET_TOKEN=xyz" > config.py Dev: git add config.py Dev: git commit -m "Add config" Moltbot: ❌ Pre-commit hook detected secret! Dev: rm config.py && git reset Dev: (add config to .gitignore and to environment variables instead) Dev: git commit -m "Add config" # Now works! ``` ### Pattern 4: Docker Image Security ``` Dev: @clawd scan-docker my-api:v1.2.3 Moltbot: ✅ Docker image clean ``` ## Configuration ### Environment Variables These are required for the skill to work: | Variable | Value | Where to Set | | :-- | :-- | :-- | | `GITGUARDIAN_API_KEY` | Your API key from https://dashboard.gitguardian.com | `~/.bashrc` or `~/.zshrc` | | `GITGUARDIAN_ENDPOINT` | `https://api.gitguardian.com` (default, optional) | Usually not needed | ### Optional ggshield Config Create `~/.gitguardian/.gitguardian.yml` for persistent settings: ```yaml verbose: false output-format: json exit-code: true ``` For details: https://docs.gitguardian.com/ggshield-docs/ ## Privacy & Security ### What Data is Sent to GitGuardian? ✅ **ONLY metadata is sent**: - Hash of the secret pattern (not the actual secret) - File path (relative path only) - Line number ❌ **NEVER sent**: - Your actual secrets or credentials - File contents - Private keys - Credentials **Reference**: GitGuardian Enterprise customers can use on-premise scanning with no data sent anywhere. ### How Secrets Are Detected ggshield uses: 1. **Entropy-based detection**: Identifies high-entropy strings (random tokens) 2. **Pattern matching**: Looks for known secret formats (AWS key prefixes, etc.) 3. **Public CVEs**: Cross-references disclosed secrets 4. **Machine learning**: Trained on leaked secrets database ## Troubleshooting ### "ggshield: command not found" ggshield is not installed or not in your PATH. **Fix**: ```bash pip install ggshield which ggshield # Should return a path ``` ### "GITGUARDIAN_API_KEY not found" The environment variable is not set. **Fix**: ```bash export GITGUARDIAN_API_KEY="your-key" # For persistence, add to ~/.bashrc or ~/.zshrc: echo 'export GITGUARDIAN_API_KEY="your-key"' >> ~/.bashrc source ~/.bashrc ``` ### "401 Unauthorized" API key is invalid or expired. **Fix**: ```bash # Test the API key ggshield auth status # If invalid, regenerate at https://dashboard.gitguardian.com → API Tokens # Then: export GITGUARDIAN_API_KEY="new-key" ``` ### "Slow on large repositories" Scanning a 50GB monorepo takes time. ggshield is doing a lot of work. **Workaround**: ```bash # Scan only staged changes (faster): @clawd scan-staged # Or specify a subdirectory: @clawd scan-file ./app/config.py ``` ## Advanced Topics ### Ignoring False Positives Sometimes ggshield flags a string that's NOT a secret (e.g., a test key): ```bash # Ignore the last secret found ggshield secret ignore --last-found # Ignore all in a file ggshield secret ignore --path ./config-example.py ``` This creates `.gitguardian/config.json` with ignore rules. ### Integrating with CI/CD You can add secret scanning to GitHub Actions / GitLab CI: ```yaml # .github/workflows/secret-scan.yml name: Secret Scan on: [push] jobs: scan: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - run: pip install ggshield - run: ggshield secret scan repo . env: GITGUARDIAN_API_KEY: ${{ secrets.GITGUARDIAN_API_KEY }} ``` ### Enterprise: On-Premise Scanning If your company uses GitGuardian Enterprise, you can scan without sending data to the cloud: ```bash export GITGUARDIAN_ENDPOINT="https://your-instance.gitguardian.com" export GITGUARDIAN_API_KEY="your-enterprise-key" ``` ## Related Resources - **ggshield Documentation**: https://docs.gitguardian.com/ggshield-docs/ - **GitGuardian Dashboard**: https://dashboard.gitguardian.com (view all secrets found) - **Moltbot Skills**: https://docs.molt.bot/tools/clawdhub - **Secret Management Best Practices**: https://cheatsheetseries.owasp.org/cheatsheets/Secrets_Management_Cheat_Sheet.html ## Support - **Bug reports**: https://github.com/GitGuardian/ggshield-skill/issues - **Questions**: Open an issue or comment on ClawdHub - **ggshield issues**: https://github.com/GitGuardian/ggshield/issues ## License MIT License - See LICENSE file ## Contributors - GitGuardian Team - [Your contributions welcome!] --- **Version**: 1.0.0 **Last updated**: January 2026 **Maintainer**: GitGuardian

path: README.md
content: # ggshield Secret Scanner

A MoltHub skill that wraps [GitGuardian's ggshield](https://github.com/GitGuardian/ggshield) CLI for detecting hardcoded secrets in your code.

## What is a MoltHub Skill?

MoltHub skills are **capabilities for AI agents** (like Cursor, Claude Code, Moltbot, etc.). When you install this skill, your AI agent gains the ability to scan code for secrets.

**This is NOT a CLI tool you run in the terminal.** Instead, you ask your AI agent to use it:

```
You: "Scan this repository for secrets"
Agent: [uses ggshield skill] ✅ Repository clean: 0 secrets found
```

The skill provides the AI agent with methods it can call on your behalf.

## What This Skill Does

Scans your code for 500+ types of hardcoded secrets before they're committed to git:

- AWS Access Keys, GCP Service Accounts, Azure credentials
- API tokens (GitHub, Slack, Stripe, OpenAI, etc.)
- Database passwords and connection strings
- Private encryption keys and certificates
- OAuth tokens and refresh tokens

## Prerequisites

This skill **wraps the ggshield CLI** - it doesn't embed it. Users need:

1. **ggshield installed**:
   ```bash
   pip install ggshield
   # or
   uv add ggshield
   ```

2. **GitGuardian API Key** (free):
   - Sign up at https://dashboard.gitguardian.com
   - Generate an API key in Settings
   - Create a `.env` file:
   ```bash
   echo 'GITGUARDIAN_API_KEY=your-api-key-here' > .env
   ```

## Installation

### From MoltHub

```bash
npx molthub@latest install ggshield-scanner
```

### Manual

Clone this repo into your skills directory:
```bash
git clone https://github.com/achillemascia/ggshield-skill.git ~/.moltbot/skills/ggshield-scanner
```

## Available Methods

| Method | Description |
|--------|-------------|
| `scan_repo(path)` | Scan entire git repository for secrets |
| `scan_file(path)` | Scan a single file |
| `scan_staged()` | Scan only staged git changes (fast pre-commit) |
| `install_hooks(type)` | Install git pre-commit or pre-push hook |
| `scan_docker(image)` | Scan Docker image layers for secrets |

## How AI Agents Use This Skill

When an AI agent has this skill installed, you can ask it to scan for secrets in natural language:

| You say | Agent does |
|---------|------------|
| "Scan this repo for secrets" | Calls `scan_repo(".")` |
| "Check if config.py has any hardcoded keys" | Calls `scan_file("config.py")` |
| "Are my staged changes safe to commit?" | Calls `scan_staged()` |
| "Set up a pre-commit hook to catch secrets" | Calls `install_hooks("pre-commit")` |
| "Scan my Docker image for secrets" | Calls `scan_docker("myapp:latest")` |

**Example conversation:**
```
You: Before I push, can you check if there are any secrets in my staged changes?

Agent: I'll scan your staged changes for secrets.

       ✅ Staged changes are clean

       No hardcoded secrets detected. Safe to commit!
```

## Local Development & Testing

### 1. Set up environment

```bash
cd ggshield-skill

# Install dependencies
uv sync

# Create .env file with your API key
echo 'GITGUARDIAN_API_KEY=your-api-key-here' > .env
```

### 2. Test the module imports

```bash
uv run python -c "from ggshield_skill import GGShieldSkill; s = GGShieldSkill(); print(s.name)"
# Output: ggshield
```

### 3. Test scanning

Create a test file with fake secrets:
```bash
mkdir -p test-data
echo 'AWS_KEY="[REDACTED_SECRET]"' > test-data/secrets.py
echo 'SLACK_TOKEN="xoxb-1234567890-abcdefghij"' >> test-data/secrets.py
```

Run a scan (loads API key from .env):
```bash
export $(cat .env | xargs) && uv run python -c "
import asyncio
from ggshield_skill import GGShieldSkill

async def test():
    skill = GGShieldSkill()
    result = await skill.scan_file('./test-data/secrets.py')
    print(result)

asyncio.run(test())
"
```

### 4. Test all methods

```bash
export $(cat .env | xargs) && uv run python -c "
import asyncio
from ggshield_skill import GGShieldSkill

async def test_all():
    s = GGShieldSkill()

    print('=== scan_repo ===')
    print(await s.scan_repo('.'))

    print('\n=== scan_file ===')
    print(await s.scan_file('./ggshield_skill.py'))

    print('\n=== scan_staged ===')
    print(await s.scan_staged())

    print('\n=== scan_file (missing - should error) ===')
    print(await s.scan_file('/nonexistent.py'))

asyncio.run(test_all())
"
```

### 5. Run with pytest (optional)

```bash
uv add --dev pytest pytest-asyncio

cat > test_skill.py << 'EOF'
import pytest
from ggshield_skill import GGShieldSkill

def test_skill_initialization():
    """
    GIVEN a GGShieldSkill class
    WHEN instantiated
    THEN it should have correct metadata
    """
    skill = GGShieldSkill()
    assert skill.name == "ggshield"
    assert skill.version == "1.0.0"
    assert skill.requires_api_key is True

@pytest.mark.asyncio
async def test_scan_file_not_found():
    """
    GIVEN a non-existent file path
    WHEN scan_file is called
    THEN it should return an error message
    """
    skill = GGShieldSkill()
    result = await skill.scan_file("/nonexistent/file.py")
    assert "File not found" in result
    assert "❌" in result

@pytest.mark.asyncio
async def test_scan_repo_not_found():
    """
    GIVEN a non-existent directory path
    WHEN scan_repo is called
    THEN it should return an error message
    """
    skill = GGShieldSkill()
    result = await skill.scan_repo("/nonexistent/dir")
    assert "Path not found" in result
    assert "❌" in result
EOF

uv run pytest test_skill.py -v
```

## Testing with Moltbot (Local Integration Test)

Before publishing, you can test the skill with a local Moltbot installation.

### 1. Install Moltbot

```bash
# Quick install
curl -fsSL https://molt.bot/install.sh | bash

# Or via npm
npm install -g moltbot@latest

# Run onboarding
moltbot onboard --install-daemon
```

### 2. Set up the API key in your shell

Add to your `~/.zshrc` or `~/.bashrc`:

```bash
export GITGUARDIAN_API_KEY="your-api-key-here"
```

Then reload: `source ~/.zshrc`

This way Moltbot picks up the key from your environment - no need to put it in config files.

### 3. Add your skill locally

```bash
# Create the managed skills folder
mkdir -p ~/.clawdbot/skills

# Symlink your skill for live development
ln -s "$(pwd)" ~/.clawdbot/skills/ggshield-scanner
```

### 4. Verify your skill is recognized

```bash
# List all skills
moltbot skills list

# Check if it's eligible (requirements met)
moltbot skills list --eligible

# Get info about your skill
moltbot skills info ggshield-scanner
```

### 5. Test with the agent

```bash
# Start an interactive chat
moltbot tui

# Or send a message directly
moltbot message "Scan the current directory for secrets"
```

## Publishing to MoltHub

### 1. Login to MoltHub

```bash
npx molthub@latest login
```

This opens a browser for GitHub authentication.

### 2. Publish

```bash
npx molthub@latest publish . \
  --slug ggshield-scanner \
  --name "ggshield Secret Scanner" \
  --version 1.0.0 \
  --changelog "Initial release: secret scanning for AI agents"
```

### 3. Verify

```bash
npx molthub@latest search "ggshield"
```

Visit: https://clawdhub.com/skills/ggshield-scanner

## Project Structure

```
ggshield-skill/
├── SKILL.md           # MoltHub metadata + documentation
├── ggshield_skill.py  # Main Python implementation
├── pyproject.toml     # uv/pip dependencies
├── README.md          # This file
├── .env               # Your API key (not committed)
├── .env.example       # Example .env file
├── .gitignore
└── LICENSE            # MIT
```

## How It Works

The skill is a Python class that:

1. **Wraps ggshield CLI** - Calls `ggshield` via subprocess
2. **Handles errors gracefully** - Missing API key, ggshield not installed, file not found
3. **Returns user-friendly messages** - With emoji indicators (✅ ❌ 🔍)
4. **Async methods** - Compatible with async AI agent frameworks

```python
class GGShieldSkill:
    async def scan_repo(self, path: str) -> str: ...
    async def scan_file(self, path: str) -> str: ...
    async def scan_staged(self) -> str: ...
    async def install_hooks(self, hook_type: str = "pre-commit") -> str: ...
    async def scan_docker(self, image: str) -> str: ...
```

## Privacy & Security

- **Local scanning** - ggshield sends only metadata (hashes, not actual secrets) to GitGuardian
- **Enterprise option** - GitGuardian Enterprise supports on-premise scanning with zero data transmission
- See [ggshield privacy docs](https://docs.gitguardian.com/ggshield-docs/) for details

## Related

- [ggshield GitHub](https://github.com/GitGuardian/ggshield)
- [GitGuardian Dashboard](https://dashboard.gitguardian.com)
- [MoltHub Registry](https://clawdhub.com)

## License

MIT


path: ggshield_skill.py
content: """
ggshield Secret Scanner Skill for Moltbot

Detects 500+ types of hardcoded secrets (API keys, credentials, certificates)
before they're committed to git.

Author: GitGuardian Team
Version: 1.0.0
"""

import os
import subprocess


class GGShieldSkill:
    """
    Moltbot skill that wraps ggshield CLI for secret scanning.

    Provides methods to scan repositories, files, staged changes, and Docker images
    for hardcoded secrets using GitGuardian's detection engine.
    """

    def __init__(self):
        """Initialize the ggshield skill."""
        self.name = "ggshield"
        self.version = "1.0.0"
        self.requires_api_key = True
        self.api_key_env = "GITGUARDIAN_API_KEY"

    def _get_api_key(self) -> str:
        """
        Get GitGuardian API key from environment.

        Raises ValueError if not set.
        """
        [REDACTED_SECRET])
        if not api_key:
            raise ValueError(
                f"{self.api_key_env} not set. "
                "Get one at https://dashboard.gitguardian.com"
            )
        return api_key

    def _run_ggshield(self, *args: str) -> tuple[int, str, str]:
        """
        Run ggshield CLI command with API key.

        Args:
            *args: Arguments to pass to ggshield (e.g., 'secret', 'scan', 'repo', '.')

        Returns:
            Tuple of (exit_code, stdout, stderr)

        Raises:
            FileNotFoundError: If ggshield is not installed
        """
        [REDACTED_SECRET])
        command = ["ggshield", *args]

        env = {**os.environ, self.api_key_env: api_key}

        result = subprocess.run(
            command,
            capture_output=True,
            text=True,
            env=env,
        )

        return result.returncode, result.stdout, result.stderr

    def _format_success(self, message: str) -> str:
        """Format success message with emoji."""
        return f"✅ {message}"

    def _format_error(self, message: str) -> str:
        """Format error message with emoji."""
        return f"❌ {message}"

    def _format_scanning(self, message: str) -> str:
        """Format scanning message with emoji."""
        return f"🔍 {message}"

    def _is_git_repository(self, path: str = ".") -> bool:
        """Check if the given path is inside a git repository."""
        git_dir = os.path.join(path, ".git")
        if os.path.exists(git_dir):
            return True
        # Also check if we're in a subdirectory of a git repo
        try:
            result = subprocess.run(
                ["git", "rev-parse", "--git-dir"],
                capture_output=True,
                text=True,
                cwd=path,
            )
            return result.returncode == 0
        except (FileNotFoundError, OSError):
            return False

    async def scan_repo(self, path: str) -> str:
        """
        Scan entire git repository for secrets.

        Args:
            path: Path to repository root

        Returns:
            User-facing message describing results
        """
        # Validate path exists
        if not os.path.exists(path):
            return self._format_error(f"Path not found: {path}")

        if not os.path.isdir(path):
            return self._format_error(f"Not a directory: {path}")

        try:
            exit_code, stdout, stderr = self._run_ggshield(
                "secret", "scan", "repo", path
            )

            if exit_code == 0:
                return self._format_success(f"Repository clean: {path}")
            else:
                # Secrets were found
                output = stdout if stdout else stderr
                return self._format_error(f"Secrets found in repository:\n{output}")

        except FileNotFoundError:
            return self._format_error(
                "ggshield not installed. Run: pip install ggshield"
            )
        except ValueError as e:
            return self._format_error(str(e))
        except Exception as e:
            return self._format_error(f"Unexpected error: {e}")

    async def scan_file(self, path: str) -> str:
        """
        Scan a single file for secrets.

        Args:
            path: Path to file to scan

        Returns:
            User-facing message describing results
        """
        # Validate file exists
        if not os.path.exists(path):
            return self._format_error(f"File not found: {path}")

        if not os.path.isfile(path):
            return self._format_error(f"Not a file: {path}")

        try:
            exit_code, stdout, stderr = self._run_ggshield("secret", "scan", "path", path)

            if exit_code == 0:
                return self._format_success(f"File clean: {path}")
            else:
                # Secrets were found
                output = stdout if stdout else stderr
                return self._format_error(f"Secrets found in {path}:\n{output}")

        except FileNotFoundError:
            return self._format_error(
                "ggshield not installed. Run: pip install ggshield"
            )
        except ValueError as e:
            return self._format_error(str(e))
        except Exception as e:
            return self._format_error(f"Unexpected error: {e}")

    async def scan_staged(self) -> str:
        """
        Scan only staged git changes (pre-commit mode).

        Fast scanning of what's about to be committed. Requires git repository.

        Returns:
            User-facing message describing results
        """
        # Check we're in a git repository
        if not self._is_git_repository():
            return self._format_error(
                "Not in a git repository. Run from repo root."
            )

        try:
            exit_code, stdout, stderr = self._run_ggshield(
                "secret", "scan", "pre-commit"
            )

            if exit_code == 0:
                return self._format_success("Staged changes are clean")
            else:
                # Secrets were found
                output = stdout if stdout else stderr
                return self._format_error(
                    f"Secrets detected in staged changes:\n{output}"
                )

        except FileNotFoundError:
            return self._format_error(
                "ggshield not installed. Run: pip install ggshield"
            )
        except ValueError as e:
            return self._format_error(str(e))
        except Exception as e:
            return self._format_error(f"Unexpected error: {e}")

    async def install_hooks(self, hook_type: str = "pre-commit") -> str:
        """
        Install ggshield as a git pre-commit hook.

        After installation, every commit will be scanned automatically.

        Args:
            hook_type: Type of hook to install: 'pre-commit' or 'pre-push'

        Returns:
            User-facing message describing installation result
        """
        # Validate hook type
        valid_hooks = ("pre-commit", "pre-push")
        if hook_type not in valid_hooks:
            return self._format_error(
                f"Invalid hook type: {hook_type}. Must be one of: {', '.join(valid_hooks)}"
            )

        # Check we're in a git repository
        if not self._is_git_repository():
            return self._format_error(
                "Not in a git repository. Run from repo root."
            )

        try:
            exit_code, stdout, stderr = self._run_ggshield(
                "install", "--mode", "local", "--hook-type", hook_type
            )

            if exit_code == 0:
                return self._format_success(
                    f"Installed {hook_type} hook.\n"
                    f"From now on, commits with secrets will be blocked."
                )
            else:
                output = stderr if stderr else stdout
                return self._format_error(
                    f"Failed to install hook: {output}"
                )

        except FileNotFoundError:
            return self._format_error(
                "ggshield not installed. Run: pip install ggshield"
            )
        except ValueError as e:
            return self._format_error(str(e))
        except Exception as e:
            return self._format_error(f"Unexpected error: {e}")

    async def scan_docker(self, image: str) -> str:
        """
        Scan Docker image for secrets in its layers.

        Args:
            image: Docker image name/tag (e.g., 'myapp:latest')

        Returns:
            User-facing message describing results
        """
        # Validate image string
        if not image or not image.strip():
            return self._format_error("Docker image name is required")

        image = image.strip()

        try:
            exit_code, stdout, stderr = self._run_ggshield(
                "secret", "scan", "docker", image
            )

            if exit_code == 0:
                return self._format_success(f"Docker image {image} is clean")
            else:
                # Check for Docker not available error
                combined_output = f"{stdout}\n{stderr}".lower()
                if "docker" in combined_output and (
                    "not found" in combined_output
                    or "cannot connect" in combined_output
                    or "is not running" in combined_output
                ):
                    return self._format_error(
                        "Docker is not available. Make sure Docker is installed and running."
                    )

                # Check for image not found
                if "no such image" in combined_output or "not found" in combined_output:
                    return self._format_error(
                        f"Docker image not found: {image}. "
                        "Make sure the image exists locally or pull it first."
                    )

                # Secrets were found
                output = stdout if stdout else stderr
                return self._format_error(
                    f"Secrets found in Docker image {image}:\n{output}"
                )

        except FileNotFoundError:
            return self._format_error(
                "ggshield not installed. Run: pip install ggshield"
            )
        except ValueError as e:
            return self._format_error(str(e))
        except Exception as e:
            return self._format_error(f"Unexpected error: {e}")


path: pyproject.toml
content: [project]
name = "ggshield-skill"
version = "1.0.0"
description = "Moltbot skill for secret scanning using GitGuardian's ggshield"
readme = "SKILL.md"
license = { text = "MIT" }
authors = [{ name = "GitGuardian Team" }]
requires-python = ">=3.10"
dependencies = [
    "ggshield>=1.15.0",
    "pygitguardian>=1.25.0",
]

[project.urls]
Homepage = "https://github.com/achillemascia/ggshield-skill"
Repository = "https://github.com/achillemascia/ggshield-skill"

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"


Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: suspicious | SkillSpector: clean (LOW)