Skip to main content

Valca

AI coding security co-pilot — blocks insecure code at the moment of generation.

Formerly published as vigilsec. The package is now valca. Both the valca and vigil commands work, so existing hooks and scripts keep running unchanged.

Valca intercepts every file an AI coding assistant writes and blocks it if CRITICAL or HIGH security findings are detected — before the file hits disk. It's the only tool that operates at generation time rather than post-commit.

AI writes file → vigil scan → exit 2 → Claude Code blocks the write

The Problem

AI coding assistants reproduce the most common patterns in their training data. The most common patterns are insecure defaults.

The clearest example: every existing IaC scanner (Checkov, Trivy, Snyk, Semgrep) misses the docker-compose port binding that exposes your database to the internet:

ports:
  - "5432:5432"   # ← binds to 0.0.0.0, bypasses UFW, reachable from anywhere

The correct form is "127.0.0.1:5432:5432". Vigil catches it. Nothing else does.


Install

pip install valca

Wire the Claude Code hook (one time):

valca init --global     # `vigil init --global` also works

That's it. Every file Claude Code writes is now scanned before it saves. Reload Claude Code to activate.


Network access

Valca's core scanning is fully offline — the package has zero runtime dependencies and the engine never sends your code, file paths, or findings anywhere.

Three rules do reach the network, because checking whether a dependency is vulnerable or fabricated is impossible offline. When a manifest (requirements.txt, package.json, lockfiles) is scanned, these rules send package names and version strings only to:

Host Used by What is sent
api.osv.dev VGL-PKG001 (known CVE in a pinned version) package name + version
pypi.org VGL-PKG002/003/004 (hallucinated, stale, or suspicious package) package name
registry.npmjs.org VGL-PKG002/003/004 package name

Your source code, file contents, file paths, and scan results are never transmitted. If your dependency inventory is itself sensitive, turn these rules off in .vigilrc and Valca runs completely offline:

disabled_rules = ["VGL-PKG001", "VGL-PKG002", "VGL-PKG003", "VGL-PKG004"]

Usage

# Scan a single file
vigil scan docker-compose.yml

# Scan a directory
vigil scan ./my-project/

# JSON output (for CI / dashboards)
vigil scan ./my-project/ --format json

# SARIF output (for GitHub Advanced Security)
vigil scan ./my-project/ --format sarif > results.sarif

# Only report HIGH and above
vigil scan ./my-project/ --severity HIGH

# Open feedback & waitlist form
vigil feedback

Review what has been caught over time. Both commands read the local scan history — nothing leaves your machine.

# Findings log — what was caught, where, and when
valca log                              # 20 most recent findings
valca log --severity CRITICAL          # only criticals
valca log --project api --since 2026-09-01
valca log --limit 100 --format json    # for dashboards

# Aggregate stats — rule frequency, severity mix, precision per rule
valca stats
valca stats --format json

valca stats reports how often each rule fires and how often you suppressed it, so rules with poor precision in your codebase are visible rather than guessed at.

Exit codes:

Code Meaning
0 No findings — write proceeds
1 Advisory findings only (MEDIUM / LOW / INFO)
2 CRITICAL or HIGH found — Claude Code blocks the write

See It in Action

Blocking a vulnerable GitHub Actions workflow at write time:

Valca blocking a Comment-and-Control attack

In April 2026, researchers found that all three major AI coding agents (Claude Code, Gemini CLI, Copilot) could be hijacked to exfiltrate ANTHROPIC_API_KEY and GITHUB_TOKEN via a hidden HTML comment in a GitHub issue. CVSS 9.4. No special access required.

Vigil catches the vulnerable workflow (issues: trigger + AI agent + API key in env) before it reaches git — the only tool that does.

Full writeup: The Attack That Steals Your API Keys Through a GitHub Issue Comment


Rules

102 rules across 26 categories. All built-in, stdlib-only, zero runtime dependencies.

This catalogue is generated from the rule registry — it cannot drift from the shipped engine.

Secrets & Credential Exposure (14 rules)

Rule Severity What it catches
VGL-I001 CRITICAL eval() or exec() — code injection risk
VGL-I002 HIGH subprocess with shell=True
VGL-I003 HIGH os.system() call
VGL-S001 CRITICAL AWS access key hardcoded
VGL-S002 CRITICAL Hardcoded password
VGL-S003 CRITICAL Hardcoded API key
VGL-S004 CRITICAL Hardcoded bearer token
VGL-S005 CRITICAL Hardcoded JWT secret
VGL-S006 CRITICAL PEM private key in source
VGL-S007 CRITICAL Database URL with embedded credentials
VGL-S008 CRITICAL Stripe live secret key
VGL-S009 CRITICAL Slack token hardcoded
VGL-S010 CRITICAL Provider API key hardcoded (OpenAI / GitHub / GitLab / Google)
VGL-S011 HIGH Insecure placeholder default for security-critical config

GitHub Actions — AI Agent Surface (9 rules)

Rule Severity What it catches
VGL-GHA001 CRITICAL Pwn Request — pull_request_target with attacker-controlled checkout ref
VGL-GHA002 CRITICAL Script injection — user-controlled context expression in run: step
VGL-GHA004 HIGH Secret directly interpolated in run: step (visible in process table)
VGL-GHA005 MEDIUM Workflow missing explicit permissions block (implicit GITHUB_TOKEN scope)
VGL-GHA006 HIGH Cache usage in pull_request workflow (cache poisoning attack vector)
VGL-GHA007 HIGH Self-hosted runner with pull_request trigger (persistent runner risk)
VGL-GHA008 HIGH workflow_run trigger without ref/repo validation
VGL-GHA009 CRITICAL AI agent wired to untrusted-input trigger (issues/pull_request_target) with API key in env
VGL-GHA010 HIGH AI agent on pull_request_target without fork origin guard

Dockerfile Hardening (8 rules)

Rule Severity What it catches
VGL-DF001 HIGH Dockerfile runs as root — no USER directive
VGL-DF002 MEDIUM Dockerfile uses unpinned :latest base image
VGL-DF003 HIGH Dockerfile bakes secret into ENV or ARG layer
VGL-DF004 HIGH curl
VGL-DF005 HIGH TLS verification disabled in Dockerfile RUN fetch
VGL-DF006 MEDIUM ADD used for local files — use COPY instead
VGL-DF007 HIGH COPY . without .dockerignore — risks leaking .git, .env, credentials
VGL-DF008 MEDIUM World-writable permissions set in Dockerfile (chmod 777)

Docker Compose (7 rules)

Rule Severity What it catches
VGL-D001 CRITICAL Docker public port binding — bypasses UFW, exposes service to internet
VGL-D002 HIGH docker-compose environment block contains hardcoded secret
VGL-D003 CRITICAL Docker container runs in privileged mode
VGL-D004 HIGH Docker container uses host network mode
VGL-D005 CRITICAL Docker socket mounted into container — full container escape vector
VGL-D006 HIGH Sensitive host path mounted into container
VGL-D007 HIGH AWS credentials directory (~/.aws) mounted into container — exposes all profiles

Terraform (7 rules)

Rule Severity What it catches
VGL-TF001 CRITICAL Terraform hardcoded secret value
VGL-TF002 HIGH Terraform resource with public access enabled
VGL-TF003 HIGH Terraform encryption explicitly disabled
VGL-TF004 CRITICAL IMDSv1 enabled on EC2 instance — vulnerable to SSRF metadata theft
VGL-TF005 HIGH Terraform S3 state backend stored without encryption
VGL-TF006 MEDIUM Deletion protection disabled on managed resource
VGL-TF007 MEDIUM Audit logging disabled on Terraform-managed resource

Deserialization & Path Traversal (5 rules)

Rule Severity What it catches
VGL-DESER001 CRITICAL Insecure deserialization — pickle.loads / pickle.load
VGL-DESER002 HIGH Insecure YAML deserialization — yaml.load() without SafeLoader
VGL-DESER003 HIGH Insecure deserialization — marshal.loads with untrusted data
VGL-PATH001 HIGH Path traversal — user input passed to file open or path join without validation
VGL-SSTI001 CRITICAL Server-Side Template Injection — user input rendered as Jinja2 template

Web Application Security (5 rules)

Rule Severity What it catches
VGL-CORS001 HIGH CORS wildcard — allow_origins=['*']
VGL-SQL001 CRITICAL SQL injection — query built with f-string or string concatenation
VGL-SQL002 CRITICAL SQL injection — ORM raw query with f-string
VGL-SSL001 HIGH SSL verification disabled
VGL-SSRF001 CRITICAL SSRF — HTTP call with user-controlled URL

AI Agent — Excessive Agency (4 rules)

Rule Severity What it catches
VGL-A001 CRITICAL LLM output piped into shell execution
VGL-A002 HIGH Hardcoded auto-approval disables human-in-the-loop
VGL-A003 HIGH Unbounded agent loop with no iteration limit
VGL-A004 HIGH LLM response written to disk without validation

AI Agent — Prompt Injection (4 rules)

Rule Severity What it catches
VGL-PI001 CRITICAL User input interpolated into the LLM system prompt
VGL-PI002 HIGH Raw HTTP request body used as LLM message content
VGL-PI003 HIGH String formatting used to build prompts with user data
VGL-PI004 MEDIUM Tool output appended to the conversation without sanitization

Authentication & Session (4 rules)

Rule Severity What it catches
VGL-AUTH001 CRITICAL JWT algorithm=none — signature verification bypassed
VGL-AUTH002 CRITICAL JWT verify_signature disabled — any token accepted
VGL-AUTH003 HIGH Weak or hardcoded web framework secret key
VGL-AUTH004 HIGH Debug mode enabled in framework code

Dependency Integrity (4 rules)

Rule Severity What it catches
VGL-PKG001 CRITICAL Package audit (CVE · hallucination · staleness · supply chain)
VGL-PKG002 CRITICAL Package not found on registry — hallucinated or slopsquatting target
VGL-PKG003 HIGH Package version significantly behind latest — stale AI training data
VGL-PKG004 HIGH Package newly registered with few releases — supply-chain risk

Kubernetes (4 rules)

Rule Severity What it catches
VGL-K001 CRITICAL Privileged container or host namespace access
VGL-K002 CRITICAL allowPrivilegeEscalation enabled in Kubernetes securityContext
VGL-K003 HIGH Dangerous Linux capabilities added in Kubernetes securityContext
VGL-K004 HIGH Sensitive hostPath volume in Kubernetes manifest

Logging & Data Exposure (4 rules)

Rule Severity What it catches
VGL-LOG001 HIGH Sensitive data written to logs (CWE-532)
VGL-LOG002 HIGH Error details leaked in HTTP response body (CWE-209)
VGL-LOG003 MEDIUM Silent exception swallowing in authentication/security context
VGL-LOG004 MEDIUM CRLF injection risk — user-controlled input logged without newline sanitization

Swift / iOS (4 rules)

Rule Severity What it catches
VGL-SW001 CRITICAL Hardcoded secret in Swift string literal
VGL-SW002 HIGH Plain HTTP URL in Swift networking code
VGL-SW003 HIGH Sensitive value written to UserDefaults (unencrypted)
VGL-SW004 CRITICAL SSL certificate validation bypassed in URLSession delegate

Dependency CVE Scanners (3 rules)

Rule Severity What it catches
VGL-DEP001 HIGH Vulnerable Python packages (pip-audit)
VGL-DEP002 HIGH Critical npm vulnerability (npm audit)
VGL-DEP003 HIGH Vulnerable lockfile package (osv-scanner)

GitHub Actions — Workflow Hygiene (3 rules)

Rule Severity What it catches
VGL-GH001 HIGH GitHub Actions secret printed in run step (log exposure)
VGL-GH002 HIGH GitHub Actions workflow with excessive permissions
VGL-GH003 HIGH GitHub Actions uses mutable action ref (tag or branch)

MCP Server Security (3 rules)

Rule Severity What it catches
VGL-MCP001 CRITICAL Prompt injection embedded in MCP tool description
VGL-MCP002 HIGH MCP tool description built from user-controlled data
VGL-MCP003 HIGH Shell execution in an MCP tool handler without a sandbox

JavaScript / TypeScript (2 rules)

Rule Severity What it catches
VGL-JS001 HIGH process.env secret with hardcoded string fallback
VGL-JS004 HIGH eval() or new Function() with dynamic argument (CWE-95)

Row-Level Security (2 rules)

Rule Severity What it catches
VGL-RLS001 CRITICAL PostgreSQL Row-Level Security explicitly disabled
VGL-RLS002 HIGH Multi-tenant ORM query missing user/tenant filter

Cross-Site Scripting (1 rule)

Rule Severity What it catches
VGL-XSS001 HIGH Cross-Site Scripting — unsafe HTML injection (CWE-79)

Cryptography (1 rule)

Rule Severity What it catches
VGL-RAND001 HIGH Weak randomness for security-sensitive value (CWE-330)

IAM Policies (1 rule)

Rule Severity What it catches
VGL-IAM001 CRITICAL IAM policy wildcard in Action or Resource

Python (1 rule)

Rule Severity What it catches
VGL-PY001 HIGH Debug bypass without env guard — auth/security conditionally disabled

Shell Scripts (1 rule)

Rule Severity What it catches
VGL-S011 HIGH Secret variable passed inline to subprocess/SSH (ps aux leak)

Trivy IaC Deep Scan (1 rule)

Rule Severity What it catches
VGL-T001 HIGH Trivy IaC deep scan — Dockerfile / Terraform misconfigurations

nginx (1 rule)

Rule Severity What it catches
VGL-N001 HIGH nginx config missing security headers or weak TLS

Configuration

Place a .vigilrc file in your project root (or any ancestor directory):

# .vigilrc
disabled_rules = ["VGL-T001"]        # skip trivy scan for this project
min_severity   = "HIGH"              # only report HIGH and above
exclude_paths  = ["vendor", "legacy"]
telemetry      = false               # opt out of anonymous local telemetry

Vigil walks up the directory tree to find the nearest .vigilrc. Child config always wins over parent. Monorepos can have per-project overrides alongside a workspace default.

Inline suppression — for a specific line you've reviewed and accepted:

auto_approve = True  # vigil: ignore

Same pattern as # noqa (flake8) and # nosec (bandit).


Opt-out

Vigil collects anonymous, local-only telemetry: rule ID, severity, and file extension. No file paths, no code, no identifiable data. Stored at ~/.vigil/events.jsonl — never sent anywhere.

Opt out permanently:

export VIGIL_NO_TELEMETRY=1

Or in .vigilrc:

telemetry = false

Adding a Rule

# src/vigil/rules/my_category.py
from pathlib import Path
from .base import Finding, Rule, Severity

class MyRule(Rule):
    id = "VGL-X001"
    name = "Descriptive rule name"
    severity = Severity.HIGH

    def applies_to(self, path: Path) -> bool:
        return path.suffix == ".yml"

    def check(self, path: Path) -> list[Finding]:
        findings = []
        for i, line in enumerate(path.read_text().splitlines(), 1):
            if "bad_pattern" in line:
                findings.append(Finding(
                    rule_id=self.id,
                    severity=self.severity,
                    message="Found bad pattern",
                    file_path=path,
                    line=i,
                    snippet=line.strip(),
                    fix="Do this instead.",
                ))
        return findings

Then add it to DEFAULT_RULES in src/vigil/rules/__init__.py. Write tests. Done.


GitHub Actions

Add Vigil to any CI pipeline — copy vigil-action/workflow-template.yml into your project's .github/workflows/vigil.yml:

- name: Install Vigil
  run: pip install valca --quiet

- name: Scan with Vigil
  run: vigil scan . --no-color

- name: Upload SARIF to GitHub Code Scanning
  uses: github/codeql-action/upload-sarif@v3
  with:
    sarif_file: vigil-results.sarif

Findings appear as inline annotations on PR diffs in the GitHub Security tab.


Development

git clone https://github.com/vigilsec-io/cordon.git
cd vigil
python3 -m venv venv && source venv/bin/activate
pip install -e ".[dev]"
pytest tests/ -v

License

Business Source License 1.1 — free for non-commercial use. Commercial use requires a license agreement. Converts to MIT on 2030-06-26.


Feedback

Found a false positive? Want a rule that doesn't exist yet? Building with AI agents and hitting patterns Vigil should catch?

Open an issue → github.com/vigilsec-io/cordon/issues

Or: vigil feedback

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

valca-0.3.3.tar.gz (116.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

valca-0.3.3-py3-none-any.whl (90.9 kB view details)

Uploaded Python 3

File details

Details for the file valca-0.3.3.tar.gz.

File metadata

  • Download URL: valca-0.3.3.tar.gz
  • Upload date:
  • Size: 116.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.8

File hashes

Hashes for valca-0.3.3.tar.gz
Algorithm Hash digest
SHA256 eb0efc4987ce9d44e37de0ae2c463cda4312e1ef37e623386e0a9c96b53276d7
MD5 5781bc8d2cdb6a46753241cf1622bcc4
BLAKE2b-256 5ffbb348bcbc2e8795be1d80cb88c74446621f7b301314015ed241e532fd0eb3

See more details on using hashes here.

File details

Details for the file valca-0.3.3-py3-none-any.whl.

File metadata

  • Download URL: valca-0.3.3-py3-none-any.whl
  • Upload date:
  • Size: 90.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.8

File hashes

Hashes for valca-0.3.3-py3-none-any.whl
Algorithm Hash digest
SHA256 3e89bba7edfaa157d4e6f63ceedbf33fd97b98cddc25326bec6b6379ca2c0b8e
MD5 696a27ac214768edaaa9f9d8f2f4f0b4
BLAKE2b-256 08ef1848585f063b6c5800677d4f6acc139743733047718588538d737fa7bb06

See more details on using hashes here.

Release history Release notifications | RSS feed

0.5.0

2 files

0.4.1

2 files

0.4.0

2 files

This release

0.3.3 This release

2 files

0.3.2

2 files

0.3.1

2 files

0.3.0

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page