
Prompt Governance
- 61 installs
- 451 repo stars
- Updated July 21, 2026
- borghei/claude-skills
Prompt Governance is a Claude skill for auditing prompts for injection, bias and safety, and managing a versioned catalog of approved prompts for LLM applications.
About
Prompt Governance audits prompts for security vulnerabilities, bias and safety issues and manages a versioned catalog of approved prompts. A team deploying LLM applications at scale uses it to run injection/bias/safety checks, version prompts semantically, diff versions before promotion and roll back when issues appear. It ships prompt_auditor.py and prompt_catalog_manager.py.
- Prompt auditor for injection, bias and safety checks
- Versioned prompt catalog with diff-before-promote and rollback
- Draft-to-retire prompt lifecycle and governance checklist
Prompt Governance by the numbers
- 61 all-time installs (skills.sh)
- Ranked #6,381 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
prompt-governance capabilities & compatibility
- Capabilities
- prompt engineer toolkit · security audit · llm safety
- Use cases
- security audit · orchestration
- Pricing
- Free
What prompt-governance says it does
provides tools for auditing prompts for security vulnerabilities, bias, and safety issues, plus managing a versioned catalog of approved prompts.
Essential for organizations deploying LLM-based applications at scale.
Draft -> Audit -> Review -> Approve -> Deploy -> Monitor -> Retire
npx skills add https://github.com/borghei/claude-skills --skill prompt-governanceAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 61 |
|---|---|
| repo stars | ★ 451 |
| Last updated | July 21, 2026 |
| Repository | borghei/claude-skills ↗ |
What it does
Audit prompts for injection, bias and safety issues and manage a versioned catalog of approved prompts for LLM apps.
Who is it for?
Auditing prompts for injection/bias/safety and governing an approved prompt catalog.
Skip if: Designing and evaluating prompt quality (use prompt-engineer-toolkit).
When should I use this skill?
Auditing prompts for safety or injection, managing a prompt catalog, or versioning and reviewing prompts.
What you get
Audited, versioned prompts deployed from an approved catalog with a diffable change trail and rollback.
- prompt audit findings
- versioned prompt catalog
By the numbers
- ships 2 Python tools (prompt_auditor.py, prompt_catalog_manager.py)
Files
Prompt Governance
Category: Engineering
Domain: AI Governance
Overview
The Prompt Governance skill provides tools for auditing prompts for security vulnerabilities, bias, and safety issues, plus managing a versioned catalog of approved prompts. Essential for organizations deploying LLM-based applications at scale.
Quick Start
# Audit a prompt for security and safety issues
python scripts/prompt_auditor.py --file system_prompt.txt
# Audit with specific focus
python scripts/prompt_auditor.py --text "You are a helpful assistant..." --checks injection,bias,safety
# Initialize a prompt catalog
python scripts/prompt_catalog_manager.py --init --catalog-dir ./prompts
# Add a prompt to the catalog
python scripts/prompt_catalog_manager.py --add --name "customer-support-v1" --file prompt.txt --catalog-dir ./prompts
# List all prompts in catalog
python scripts/prompt_catalog_manager.py --list --catalog-dir ./promptsTools Overview
| Tool | Purpose | Key Flags |
|---|---|---|
prompt_auditor.py | Audit prompts for injection, bias, and safety | --file, --text, --checks, --format |
prompt_catalog_manager.py | Manage versioned prompt catalog | --init, --add, --list, --diff, --catalog-dir |
Workflows
Prompt Review Process
1. Author writes or modifies a prompt 2. Run prompt_auditor.py for automated checks 3. Review findings and address critical issues 4. Add approved prompt to catalog with prompt_catalog_manager.py 5. Deploy from catalog (never from ad-hoc sources)
Prompt Versioning
1. Store all prompts in catalog with semantic versioning 2. Use --diff to compare versions before promotion 3. Maintain audit trail of all prompt changes 4. Roll back to previous versions when issues detected
Reference Documentation
- Prompt Governance Framework - Policies, review processes, and compliance requirements
Common Patterns
Prompt Lifecycle
Draft -> Audit -> Review -> Approve -> Deploy -> Monitor -> Retire
Governance Checklist
- No injection vulnerabilities
- No harmful content generation potential
- Appropriate bias mitigation
- Clear scope boundaries
- Output format constraints
- Error handling instructions
Prompt Governance Framework
Purpose
A prompt governance framework ensures that all prompts deployed in production LLM applications are secure, safe, unbiased, and effective. It provides the policies, processes, and tools needed to manage prompts as first-class artifacts.
Core Principles
1. Prompts are Code: Treat prompts with the same rigor as production code - version control, review, testing, deployment gates 2. Defense in Depth: Layer multiple safety checks rather than relying on a single mechanism 3. Least Privilege: Prompts should grant the minimum capabilities needed for the task 4. Auditability: Every prompt change should be traceable to an author, reviewer, and approval
Governance Policies
P1: Prompt Approval Required
No prompt enters production without passing automated audits and human review.
P2: Version Control Mandatory
All prompts must be stored in a versioned catalog with full change history.
P3: Regular Re-Audit
Production prompts must be re-audited quarterly or when the underlying model changes.
P4: Incident Response
When a prompt-related incident occurs, the prompt must be immediately flagged for review.
Audit Categories
1. Injection Vulnerability Assessment
- Direct injection vectors (user input in prompt)
- Indirect injection vectors (external data in prompt)
- Jailbreak resistance
- System prompt leakage risk
2. Bias Detection
- Demographic bias in instructions
- Stereotyping language
- Exclusionary terminology
- Cultural assumptions
3. Safety Assessment
- Harmful content generation potential
- PII handling compliance
- Content policy adherence
- Output scope restrictions
4. Quality Assessment
- Clarity of instructions
- Completeness of context
- Consistency of tone
- Error handling coverage
Severity Levels
| Level | Description | Required Action |
|---|---|---|
| CRITICAL | Active exploitation possible | Block deployment, immediate fix |
| HIGH | Significant risk, exploitable | Fix before deployment |
| MEDIUM | Moderate risk, specific conditions | Fix within next sprint |
| LOW | Minor concern, theoretical | Track and address at convenience |
| INFO | Suggestion for improvement | Optional optimization |
Prompt Catalog Schema
name: prompt-name
version: 1.0.0
author: author-name
status: draft|review|approved|deployed|retired
created: 2026-01-01
updated: 2026-01-15
model_target: [gpt-4o, claude-sonnet]
audit_status: passed|failed|pending
last_audit: 2026-01-15
tags: [customer-support, billing]
content: |
The actual prompt text...Review Checklist
- [ ] No user input directly concatenated into prompt
- [ ] System prompt does not leak when challenged
- [ ] No biased or exclusionary language
- [ ] PII handling instructions included if applicable
- [ ] Output format and scope clearly defined
- [ ] Error cases addressed
- [ ] Token usage optimized
- [ ] Compatible with target models
#!/usr/bin/env python3
"""
Prompt Auditor - Audit prompts for injection vulnerabilities, bias, and safety issues.
Performs static analysis of prompt text to detect security vulnerabilities,
biased language, safety concerns, and quality issues.
Author: Claude Skills Engineering Team
License: MIT
"""
import argparse
import json
import re
import sys
from dataclasses import dataclass, asdict, field
from pathlib import Path
from typing import List, Dict, Optional, Set
@dataclass
class AuditFinding:
"""A single audit finding."""
category: str # injection, bias, safety, quality
severity: str # critical, high, medium, low, info
title: str
description: str
matched_text: str
recommendation: str
line_number: Optional[int] = None
@dataclass
class AuditReport:
"""Complete audit report."""
prompt_length: int
estimated_tokens: int
total_findings: int = 0
findings_by_severity: Dict[str, int] = field(default_factory=lambda: {
"critical": 0, "high": 0, "medium": 0, "low": 0, "info": 0
})
findings_by_category: Dict[str, int] = field(default_factory=dict)
findings: List[AuditFinding] = field(default_factory=list)
overall_risk: str = "low"
pass_audit: bool = True
# Injection vulnerability patterns
INJECTION_PATTERNS = [
(r'\{.*user.*\}', "critical",
"User input interpolation in prompt",
"Direct user input interpolation enables prompt injection attacks.",
"Use parameterized templates with input sanitization and delimiters."),
(r'\{.*input.*\}', "critical",
"Input variable in prompt template",
"Input variables without sanitization can carry injection payloads.",
"Sanitize all inputs. Use XML/delimiter tags to separate instructions from data."),
(r'(?:ignore|disregard|forget)\s+(?:all\s+)?(?:previous|above|prior)\s+(?:instructions|rules)',
"high",
"Anti-jailbreak instruction detected",
"The prompt contains defensive language against jailbreaks, suggesting it may be vulnerable.",
"Instead of defensive text, use structural defenses: delimiters, output validation, and layered prompts."),
(r'(?:you\s+(?:must|should)\s+)?never\s+reveal\s+(?:this|your|the)\s+(?:system\s+)?prompt',
"medium",
"System prompt concealment instruction",
"Instructing the model to hide the system prompt is a weak defense that can be bypassed.",
"Accept that system prompts may leak. Don't put secrets in prompts. Use API-level controls."),
(r'{{.*}}',
"medium",
"Double-brace template syntax",
"Template syntax could be exploited if user input is processed through the same template engine.",
"Ensure user input never passes through the template engine."),
]
# Bias detection patterns
BIAS_PATTERNS = [
(r'\b(?:he|his|him)\b(?!(?:\s+or\s+(?:she|her)))', "medium",
"Gendered language (male default)",
"Prompt uses male pronouns without inclusive alternatives.",
"Use 'they/them' or 'he/she' for inclusive language."),
(r'\b(?:chairman|businessman|fireman|policeman|mailman)\b', "medium",
"Gendered job title",
"Prompt uses gendered job titles that may introduce bias.",
"Use gender-neutral alternatives: chairperson, businessperson, firefighter, etc."),
(r'\b(?:blacklist|whitelist|master|slave)\b', "low",
"Potentially exclusionary terminology",
"Terms like blacklist/whitelist and master/slave have inclusivity concerns.",
"Use alternatives: blocklist/allowlist, primary/replica, main/secondary."),
(r'\b(?:normal|abnormal|crazy|insane|lame|dumb)\b', "low",
"Potentially ableist language",
"These terms may reinforce ableist assumptions.",
"Use specific, descriptive language instead of these colloquial terms."),
(r'\b(?:always|never|all|none|every)\b.*\b(?:people|users|customers|employees)\b', "low",
"Absolute generalization about people",
"Absolute statements about groups can reinforce stereotypes.",
"Use qualified language: 'many', 'some', 'typically' instead of absolutes."),
]
# Safety patterns
SAFETY_PATTERNS = [
(r'(?:generate|create|write|produce)\s+(?:any|all)\s+(?:content|text|output)', "high",
"Unrestricted content generation scope",
"Prompt allows unrestricted content generation without safety boundaries.",
"Add explicit content restrictions: 'Do not generate violent, sexual, or illegal content.'"),
(r'(?:no\s+(?:restrictions|limits|boundaries|constraints))', "critical",
"Explicit removal of safety restrictions",
"Prompt explicitly removes safety constraints, enabling harmful output.",
"Always maintain safety boundaries. Define allowed content scope positively."),
(r'(?:personal|private|sensitive)\s+(?:data|information|details)', "medium",
"PII handling reference without explicit policy",
"Prompt references personal data but may lack explicit handling instructions.",
"Add clear PII handling instructions: minimize collection, mask in output, don't store."),
(r'(?:medical|legal|financial)\s+advice', "high",
"Professional advice generation without disclaimers",
"Prompt may generate professional advice without appropriate disclaimers.",
"Add disclaimers: 'This is informational only. Consult a professional for advice.'"),
(r'(?:password|secret|api.?key|token|credential)', "medium",
"Credentials referenced in prompt",
"Prompt contains or references sensitive credentials.",
"Never include credentials in prompts. Use environment variables and reference by name only."),
(r'(?:execute|run|eval)\s+(?:code|command|script)', "high",
"Code execution instruction without sandboxing",
"Prompt instructs code execution without apparent sandboxing.",
"If code execution is needed, specify sandboxed environments and allowed operations."),
]
# Quality patterns
QUALITY_PATTERNS = [
(r'^.{0,50}$', "info",
"Very short prompt",
"Prompt is very short and may lack sufficient context for consistent results.",
"Consider adding context, examples, or output format specifications."),
(r'(?:etc|\.\.\.)', "info",
"Vague continuation marker",
"Using 'etc.' or '...' leaves ambiguity about expected behavior.",
"Be explicit about all expected behaviors and outputs."),
(r'(?:be\s+creative|use\s+your\s+(?:judgment|discretion))', "low",
"Subjective instruction",
"Subjective instructions lead to inconsistent outputs.",
"Provide specific criteria, examples, or constraints instead of subjective directives."),
]
SEVERITY_ORDER = {"critical": 0, "high": 1, "medium": 2, "low": 3, "info": 4}
def estimate_tokens(text: str) -> int:
"""Quick token estimation."""
return max(1, int(len(text) / 4.0 * 0.5 + len(text.split()) / 0.75 * 0.5))
def run_audit(text: str, checks: Optional[Set[str]] = None) -> AuditReport:
"""Run full prompt audit."""
all_checks = checks or {"injection", "bias", "safety", "quality"}
report = AuditReport(
prompt_length=len(text),
estimated_tokens=estimate_tokens(text),
)
lines = text.split("\n")
pattern_groups = []
if "injection" in all_checks:
pattern_groups.append(("injection", INJECTION_PATTERNS))
if "bias" in all_checks:
pattern_groups.append(("bias", BIAS_PATTERNS))
if "safety" in all_checks:
pattern_groups.append(("safety", SAFETY_PATTERNS))
if "quality" in all_checks:
pattern_groups.append(("quality", QUALITY_PATTERNS))
for category, patterns in pattern_groups:
for pattern_str, severity, title, desc, rec in patterns:
try:
pattern = re.compile(pattern_str, re.IGNORECASE)
except re.error:
continue
for i, line in enumerate(lines, 1):
if pattern.search(line):
match = pattern.search(line)
matched_text = match.group()[:80] if match else line.strip()[:80]
report.findings.append(AuditFinding(
category=category,
severity=severity,
title=title,
description=desc,
matched_text=matched_text,
recommendation=rec,
line_number=i,
))
# Check for missing safety elements
if "safety" in all_checks:
full_lower = text.lower()
if "do not" not in full_lower and "don't" not in full_lower and "must not" not in full_lower:
report.findings.append(AuditFinding(
category="safety",
severity="info",
title="No negative constraints found",
description="Prompt lacks explicit 'do not' constraints. Consider adding boundaries.",
matched_text="[entire prompt]",
recommendation="Add explicit restrictions on what the model should NOT do.",
))
if "format" not in full_lower and "json" not in full_lower and "structure" not in full_lower:
report.findings.append(AuditFinding(
category="quality",
severity="info",
title="No output format specification",
description="Prompt doesn't specify an output format, leading to inconsistent responses.",
matched_text="[entire prompt]",
recommendation="Add output format instructions (e.g., JSON schema, markdown template).",
))
# Aggregate
report.total_findings = len(report.findings)
for f in report.findings:
report.findings_by_severity[f.severity] = report.findings_by_severity.get(f.severity, 0) + 1
report.findings_by_category[f.category] = report.findings_by_category.get(f.category, 0) + 1
report.findings.sort(key=lambda x: SEVERITY_ORDER.get(x.severity, 4))
# Determine overall risk and pass/fail
if report.findings_by_severity.get("critical", 0) > 0:
report.overall_risk = "critical"
report.pass_audit = False
elif report.findings_by_severity.get("high", 0) > 0:
report.overall_risk = "high"
report.pass_audit = False
elif report.findings_by_severity.get("medium", 0) > 0:
report.overall_risk = "medium"
report.pass_audit = True
else:
report.overall_risk = "low"
report.pass_audit = True
return report
def format_human(report: AuditReport) -> str:
"""Format for human reading."""
lines = []
lines.append("=" * 65)
lines.append("PROMPT AUDIT REPORT")
lines.append("=" * 65)
lines.append(f"Prompt length: {report.prompt_length} chars, ~{report.estimated_tokens} tokens")
lines.append(f"Total findings: {report.total_findings}")
verdict = "PASS" if report.pass_audit else "FAIL"
lines.append(f"Overall risk: {report.overall_risk.upper()}")
lines.append(f"Audit result: {verdict}")
lines.append("")
lines.append("Findings by Severity:")
for sev in ["critical", "high", "medium", "low", "info"]:
count = report.findings_by_severity.get(sev, 0)
if count > 0:
lines.append(f" {sev.upper()}: {count}")
lines.append("")
if report.findings_by_category:
lines.append("Findings by Category:")
for cat, count in sorted(report.findings_by_category.items()):
lines.append(f" {cat}: {count}")
lines.append("")
for i, f in enumerate(report.findings, 1):
lines.append("-" * 50)
ln = f" (line {f.line_number})" if f.line_number else ""
lines.append(f"[{i}] [{f.severity.upper()}] [{f.category.upper()}] {f.title}{ln}")
lines.append(f" Match: {f.matched_text}")
lines.append(f" Issue: {f.description}")
lines.append(f" Fix: {f.recommendation}")
lines.append("")
if report.total_findings == 0:
lines.append("No issues found. Prompt passes audit.")
lines.append("=" * 65)
return "\n".join(lines)
def format_json(report: AuditReport) -> str:
"""Format as JSON."""
data = {
"prompt_length": report.prompt_length,
"estimated_tokens": report.estimated_tokens,
"total_findings": report.total_findings,
"overall_risk": report.overall_risk,
"pass_audit": report.pass_audit,
"findings_by_severity": report.findings_by_severity,
"findings_by_category": report.findings_by_category,
"findings": [asdict(f) for f in report.findings],
}
return json.dumps(data, indent=2)
def main():
parser = argparse.ArgumentParser(
description="Prompt Auditor - Audit prompts for injection, bias, and safety issues"
)
input_group = parser.add_mutually_exclusive_group(required=True)
input_group.add_argument("--file", help="Path to prompt file")
input_group.add_argument("--text", help="Prompt text to audit")
input_group.add_argument("--stdin", action="store_true", help="Read from stdin")
parser.add_argument("--checks", help="Comma-separated checks to run: injection,bias,safety,quality (default: all)")
parser.add_argument("--format", choices=["human", "json"], default="human",
help="Output format (default: human)")
args = parser.parse_args()
if args.file:
path = Path(args.file)
if not path.exists():
print(f"Error: File not found: {args.file}", file=sys.stderr)
sys.exit(1)
text = path.read_text(encoding="utf-8", errors="ignore")
elif args.text:
text = args.text
else:
text = sys.stdin.read()
checks = None
if args.checks:
checks = set(args.checks.split(","))
report = run_audit(text, checks)
if args.format == "json":
print(format_json(report))
else:
print(format_human(report))
sys.exit(0 if report.pass_audit else 1)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
Prompt Catalog Manager - Manage a catalog of approved prompts with versioning.
Provides a local file-based prompt catalog with versioning, metadata tracking,
diff comparison, and search capabilities.
Author: Claude Skills Engineering Team
License: MIT
"""
import argparse
import hashlib
import json
import os
import sys
from datetime import datetime
from dataclasses import dataclass, asdict, field
from pathlib import Path
from typing import List, Dict, Optional
import difflib
CATALOG_FILE = "catalog.json"
PROMPTS_DIR = "versions"
@dataclass
class PromptEntry:
"""A single prompt entry in the catalog."""
name: str
version: str
status: str # draft, review, approved, deployed, retired
author: str
created: str
updated: str
description: str
tags: List[str]
model_targets: List[str]
content_hash: str
content_file: str
audit_status: str # pending, passed, failed
token_estimate: int
@dataclass
class Catalog:
"""The prompt catalog."""
created: str
updated: str
total_prompts: int = 0
entries: Dict[str, List[PromptEntry]] = field(default_factory=dict) # name -> versions
def _hash_content(content: str) -> str:
"""SHA-256 hash of content."""
return hashlib.sha256(content.encode("utf-8")).hexdigest()[:16]
def _estimate_tokens(text: str) -> int:
"""Quick token estimate."""
return max(1, int(len(text) / 4.0 * 0.5 + len(text.split()) / 0.75 * 0.5))
def _next_version(versions: List[str]) -> str:
"""Calculate next semantic version."""
if not versions:
return "1.0.0"
latest = sorted(versions, key=lambda v: [int(x) for x in v.split(".")])[-1]
parts = latest.split(".")
parts[-1] = str(int(parts[-1]) + 1)
return ".".join(parts)
def load_catalog(catalog_dir: Path) -> Catalog:
"""Load catalog from disk."""
catalog_path = catalog_dir / CATALOG_FILE
if not catalog_path.exists():
return Catalog(
created=datetime.now().isoformat(),
updated=datetime.now().isoformat(),
)
data = json.loads(catalog_path.read_text(encoding="utf-8"))
catalog = Catalog(
created=data.get("created", ""),
updated=data.get("updated", ""),
total_prompts=data.get("total_prompts", 0),
)
for name, versions in data.get("entries", {}).items():
catalog.entries[name] = [
PromptEntry(**v) for v in versions
]
return catalog
def save_catalog(catalog: Catalog, catalog_dir: Path):
"""Save catalog to disk."""
catalog.updated = datetime.now().isoformat()
catalog.total_prompts = sum(len(v) for v in catalog.entries.values())
data = {
"created": catalog.created,
"updated": catalog.updated,
"total_prompts": catalog.total_prompts,
"entries": {
name: [asdict(e) for e in versions]
for name, versions in catalog.entries.items()
},
}
catalog_path = catalog_dir / CATALOG_FILE
catalog_path.write_text(json.dumps(data, indent=2), encoding="utf-8")
def init_catalog(catalog_dir: Path) -> str:
"""Initialize a new prompt catalog."""
catalog_dir.mkdir(parents=True, exist_ok=True)
(catalog_dir / PROMPTS_DIR).mkdir(exist_ok=True)
catalog = Catalog(
created=datetime.now().isoformat(),
updated=datetime.now().isoformat(),
)
save_catalog(catalog, catalog_dir)
return f"Catalog initialized at {catalog_dir}"
def add_prompt(catalog_dir: Path, name: str, content: str,
author: str = "unknown", description: str = "",
tags: Optional[List[str]] = None,
model_targets: Optional[List[str]] = None,
status: str = "draft") -> str:
"""Add or update a prompt in the catalog."""
catalog = load_catalog(catalog_dir)
versions_dir = catalog_dir / PROMPTS_DIR
versions_dir.mkdir(exist_ok=True)
# Determine version
existing_versions = [e.version for e in catalog.entries.get(name, [])]
version = _next_version(existing_versions)
# Save content file
content_hash = _hash_content(content)
content_file = f"{name}_v{version.replace('.', '_')}.txt"
(versions_dir / content_file).write_text(content, encoding="utf-8")
# Check for duplicate content
for existing in catalog.entries.get(name, []):
if existing.content_hash == content_hash:
return f"Duplicate: Content identical to {name} v{existing.version}. No new version created."
entry = PromptEntry(
name=name,
version=version,
status=status,
author=author,
created=datetime.now().isoformat(),
updated=datetime.now().isoformat(),
description=description,
tags=tags or [],
model_targets=model_targets or [],
content_hash=content_hash,
content_file=content_file,
audit_status="pending",
token_estimate=_estimate_tokens(content),
)
if name not in catalog.entries:
catalog.entries[name] = []
catalog.entries[name].append(entry)
save_catalog(catalog, catalog_dir)
return f"Added {name} v{version} ({entry.token_estimate} tokens, hash: {content_hash})"
def list_prompts(catalog_dir: Path, show_all_versions: bool = False) -> str:
"""List all prompts in the catalog."""
catalog = load_catalog(catalog_dir)
if not catalog.entries:
return "Catalog is empty."
lines = []
lines.append(f"Prompt Catalog ({catalog.total_prompts} total entries)")
lines.append(f"Last updated: {catalog.updated}")
lines.append("=" * 70)
for name in sorted(catalog.entries.keys()):
versions = catalog.entries[name]
latest = versions[-1]
lines.append(f"\n {name}")
lines.append(f" Latest: v{latest.version} ({latest.status})")
lines.append(f" Author: {latest.author}")
lines.append(f" Tokens: ~{latest.token_estimate}")
lines.append(f" Audit: {latest.audit_status}")
lines.append(f" Tags: {', '.join(latest.tags) if latest.tags else 'none'}")
lines.append(f" Versions: {len(versions)}")
if show_all_versions and len(versions) > 1:
for v in versions:
lines.append(f" v{v.version} ({v.status}) - {v.created[:10]} [{v.audit_status}]")
return "\n".join(lines)
def diff_versions(catalog_dir: Path, name: str,
version_a: Optional[str] = None,
version_b: Optional[str] = None) -> str:
"""Show diff between two versions of a prompt."""
catalog = load_catalog(catalog_dir)
versions_dir = catalog_dir / PROMPTS_DIR
if name not in catalog.entries:
return f"Prompt '{name}' not found in catalog."
versions = catalog.entries[name]
if len(versions) < 2 and not (version_a and version_b):
return f"Only one version of '{name}' exists. Nothing to diff."
# Default to last two versions
if not version_a:
entry_a = versions[-2]
else:
entry_a = next((v for v in versions if v.version == version_a), None)
if not entry_a:
return f"Version {version_a} not found for '{name}'."
if not version_b:
entry_b = versions[-1]
else:
entry_b = next((v for v in versions if v.version == version_b), None)
if not entry_b:
return f"Version {version_b} not found for '{name}'."
file_a = versions_dir / entry_a.content_file
file_b = versions_dir / entry_b.content_file
if not file_a.exists() or not file_b.exists():
return "Content files not found on disk."
text_a = file_a.read_text(encoding="utf-8").splitlines(keepends=True)
text_b = file_b.read_text(encoding="utf-8").splitlines(keepends=True)
diff = difflib.unified_diff(
text_a, text_b,
fromfile=f"{name} v{entry_a.version}",
tofile=f"{name} v{entry_b.version}",
)
diff_text = "".join(diff)
if not diff_text:
return f"No differences between v{entry_a.version} and v{entry_b.version}."
lines = []
lines.append(f"Diff: {name} v{entry_a.version} -> v{entry_b.version}")
lines.append(f"Tokens: {entry_a.token_estimate} -> {entry_b.token_estimate} "
f"({entry_b.token_estimate - entry_a.token_estimate:+d})")
lines.append("-" * 50)
lines.append(diff_text)
return "\n".join(lines)
def update_status(catalog_dir: Path, name: str, status: str,
version: Optional[str] = None) -> str:
"""Update the status of a prompt."""
catalog = load_catalog(catalog_dir)
if name not in catalog.entries:
return f"Prompt '{name}' not found."
valid_statuses = {"draft", "review", "approved", "deployed", "retired"}
if status not in valid_statuses:
return f"Invalid status '{status}'. Must be one of: {', '.join(valid_statuses)}"
versions = catalog.entries[name]
if version:
entry = next((v for v in versions if v.version == version), None)
if not entry:
return f"Version {version} not found."
else:
entry = versions[-1]
old_status = entry.status
entry.status = status
entry.updated = datetime.now().isoformat()
save_catalog(catalog, catalog_dir)
return f"Updated {name} v{entry.version}: {old_status} -> {status}"
def search_prompts(catalog_dir: Path, query: str) -> str:
"""Search prompts by name, tags, or description."""
catalog = load_catalog(catalog_dir)
query_lower = query.lower()
results = []
for name, versions in catalog.entries.items():
latest = versions[-1]
searchable = f"{name} {latest.description} {' '.join(latest.tags)}".lower()
if query_lower in searchable:
results.append((name, latest))
if not results:
return f"No prompts matching '{query}'."
lines = [f"Search results for '{query}' ({len(results)} found):"]
for name, entry in results:
lines.append(f" {name} v{entry.version} ({entry.status}) - {entry.description[:60]}")
return "\n".join(lines)
def format_json_output(data: str) -> str:
"""Wrap string output as JSON."""
return json.dumps({"result": data}, indent=2)
def main():
parser = argparse.ArgumentParser(
description="Prompt Catalog Manager - Manage versioned prompt catalog"
)
parser.add_argument("--catalog-dir", required=True, help="Path to catalog directory")
action = parser.add_mutually_exclusive_group(required=True)
action.add_argument("--init", action="store_true", help="Initialize new catalog")
action.add_argument("--add", action="store_true", help="Add prompt to catalog")
action.add_argument("--list", action="store_true", help="List all prompts")
action.add_argument("--diff", action="store_true", help="Diff prompt versions")
action.add_argument("--status", help="Update prompt status (draft/review/approved/deployed/retired)")
action.add_argument("--search", help="Search prompts by query")
parser.add_argument("--name", help="Prompt name (for --add, --diff, --status)")
parser.add_argument("--file", help="Path to prompt content file (for --add)")
parser.add_argument("--text", help="Prompt text (for --add)")
parser.add_argument("--author", default="unknown", help="Author name")
parser.add_argument("--description", default="", help="Prompt description")
parser.add_argument("--tags", nargs="+", help="Tags for the prompt")
parser.add_argument("--models", nargs="+", help="Target models")
parser.add_argument("--version-a", help="First version for diff")
parser.add_argument("--version-b", help="Second version for diff")
parser.add_argument("--all-versions", action="store_true", help="Show all versions in list")
parser.add_argument("--format", choices=["human", "json"], default="human",
help="Output format")
args = parser.parse_args()
catalog_dir = Path(args.catalog_dir)
if args.init:
result = init_catalog(catalog_dir)
elif args.add:
if not args.name:
print("Error: --name required for --add", file=sys.stderr)
sys.exit(1)
if args.file:
content = Path(args.file).read_text(encoding="utf-8")
elif args.text:
content = args.text
else:
print("Error: --file or --text required for --add", file=sys.stderr)
sys.exit(1)
result = add_prompt(catalog_dir, args.name, content,
author=args.author, description=args.description,
tags=args.tags, model_targets=args.models)
elif args.list:
result = list_prompts(catalog_dir, show_all_versions=args.all_versions)
elif args.diff:
if not args.name:
print("Error: --name required for --diff", file=sys.stderr)
sys.exit(1)
result = diff_versions(catalog_dir, args.name, args.version_a, args.version_b)
elif args.status:
if not args.name:
print("Error: --name required for --status", file=sys.stderr)
sys.exit(1)
result = update_status(catalog_dir, args.name, args.status)
elif args.search:
result = search_prompts(catalog_dir, args.search)
else:
parser.print_help()
sys.exit(1)
if args.format == "json":
print(format_json_output(result))
else:
print(result)
if __name__ == "__main__":
main()
Related skills
FAQ
What does the prompt auditor check for?
Injection, bias and safety issues, run via prompt_auditor.py with a --checks flag such as injection,bias,safety.
What is the prompt lifecycle?
Draft, audit, review, approve, deploy, monitor, retire, with prompts deployed only from the catalog and never from ad-hoc sources.