This document provides a comprehensive guide for developing plugins for code-to-gate.
docs/plugin-guide.md is the overview and reading-order entry point.
This document owns the plugin manifest, runtime contract, SDK behavior, and
failure handling details. Copyable examples live in docs/plugin-examples.md;
sandbox and provenance boundaries live in docs/plugin-sandbox.md and
docs/plugin-security-contract.md.
code-to-gate supports a plugin architecture that allows developers to extend the core functionality with custom rules, language adapters, importers, reporters, and exporters.
| Kind | Description | Capabilities |
|---|---|---|
rule-plugin |
Custom rule evaluation | evaluate |
language-plugin |
Language parsing adapter | parse |
importer-plugin |
External tool import | import |
reporter-plugin |
Custom reporting format | report |
exporter-plugin |
Downstream export adapter | export |
Every plugin requires a manifest file (plugin-manifest.yaml or plugin-manifest.json) that defines the plugin's metadata, entry point, and capabilities.
apiVersion: ctg/v1alpha1
kind: rule-plugin
name: my-custom-rules
version: 1.0.0
visibility: public # or 'private'
description: Custom security rules for my project
entry:
command: ["node", "./dist/index.js"]
timeout: 60 # seconds
retry: 1 # retry count on failure
env:
DEBUG: "false"
capabilities:
- evaluate
receives:
- normalized-repo-graph@v1
returns:
- findings@v1
security:
network: false
filesystem:
read:
- "${repoRoot}"
write:
- "${workDir}/plugin-output"
secrets:
allow: []
dependencies:
- name: "@code-to-gate/core-rules"
version: "^0.1.0"
optional: false
metadata:
author: "Your Name"
homepage: "https://github.com/your-org/my-plugin"
license: "MIT"| Field | Type | Description |
|---|---|---|
apiVersion |
string | Must be ctg/v1alpha1 |
kind |
string | Plugin type |
name |
string | Unique identifier (lowercase alphanumeric with hyphens) |
version |
string | Semver version (e.g., 1.0.0) |
visibility |
string | public or private |
entry.command |
array | Command to execute plugin |
capabilities |
array | Plugin capabilities |
receives |
array | Input schemas |
returns |
array | Output schemas |
| Field | Type | Description |
|---|---|---|
description |
string | Plugin description |
entry.timeout |
number | Timeout in seconds (default: 60, max: 300) |
entry.retry |
number | Retry count (default: 1, max: 5) |
entry.env |
object | Environment variables |
security |
object | Security configuration |
dependencies |
array | Plugin dependencies |
metadata |
object | Additional metadata |
Plugins communicate with code-to-gate via stdin/stdout JSON.
{
"version": "ctg.plugin-input/v1",
"repo_graph": {
"version": "ctg/v1alpha1",
"files": [...],
"symbols": [...],
"relations": [...]
},
"imported_findings": {
"findings": [...]
},
"config": {
"custom": true
},
"policy": {
"blocking": {...}
},
"metadata": {
"run_id": "run-123",
"repo_root": "/path/to/repo",
"work_dir": "/path/to/work"
}
}{
"version": "ctg.plugin-output/v1",
"findings": [
{
"id": "uuid",
"ruleId": "MY_CUSTOM_RULE",
"category": "security",
"severity": "high",
"confidence": 0.85,
"title": "Security Issue Detected",
"summary": "Description of the issue",
"evidence": [
{
"id": "e1",
"path": "src/file.ts",
"startLine": 10,
"endLine": 15,
"kind": "ast"
}
],
"tags": ["security", "custom"]
}
],
"risk_seeds": [...],
"invariant_seeds": [...],
"test_seeds": [...],
"diagnostics": [
{
"id": "d1",
"severity": "info",
"code": "INFO_CODE",
"message": "Information message"
}
],
"errors": [
{
"code": "ERROR_CODE",
"message": "Error description",
"details": {...}
}
]
}Each finding must include:
| Field | Type | Required | Description |
|---|---|---|---|
id |
string (UUID) | Yes | Unique identifier |
ruleId |
string | Yes | Rule identifier (e.g., MY_RULE) |
category |
string | Yes | Category (auth, payment, validation, data, config, maintainability, testing, compatibility, release-risk, security) |
severity |
string | Yes | Severity (low, medium, high, critical) |
confidence |
number | Yes | Confidence level (0.0-1.0) |
title |
string | Yes | Short title |
summary |
string | Yes | Detailed description |
evidence |
array | Yes | Evidence references |
affectedSymbols |
array | No | Affected symbol IDs |
affectedEntrypoints |
array | No | Affected entrypoint IDs |
tags |
array | No | Tags for categorization |
upstream |
object | No | External tool reference |
Evidence must include:
| Field | Type | Required | Description |
|---|---|---|---|
id |
string | Yes | Evidence ID |
path |
string | Yes | File path |
startLine |
number | No | Start line number |
endLine |
number | No | End line number |
kind |
string | Yes | Kind (ast, text, import, external, test) |
excerptHash |
string | No | SHA-256 hash of excerpt |
nodeId |
string | No | AST node ID |
symbolId |
string | No | Symbol ID |
For TypeScript rules that should live outside the OSS core rule directory, use the Rule SDK scaffold command:
code-to-gate rule new unsafe-redirect --category security --severity highThe command writes .ctg/rules/unsafe-redirect/ by default and includes:
rule.tsimportingRulePlugin,RuleContext,Finding, and evidence helpers from@quality-harness/code-to-gate/rule-sdkrule.test.tsusingrunRuleFixturefor positive and negative fixturesfixtures/positive.tsandfixtures/negative.tsrule.manifest.jsonplusschema/rule.manifest.schema.jsonREADME.md
The scaffold is intentionally local-first. It gives rule authors a contract, fixture harness, and manifest schema before they package the rule as a process plugin or submit it to a future registry.
// dist/index.js
const fs = require('fs');
// Read input from stdin
const inputJson = fs.readFileSync(0, 'utf-8');
const input = JSON.parse(inputJson);
// Process repo graph
const findings = [];
for (const file of input.repo_graph.files) {
if (file.path.includes('dangerous-pattern')) {
findings.push({
id: generateUUID(),
ruleId: 'DANGEROUS_PATTERN',
category: 'security',
severity: 'high',
confidence: 0.9,
title: 'Dangerous pattern detected',
summary: 'File contains potentially dangerous pattern',
evidence: [{
id: 'e1',
path: file.path,
kind: 'text'
}],
tags: ['security']
});
}
}
// Write output to stdout
const output = {
version: 'ctg.plugin-output/v1',
findings,
diagnostics: [{
id: 'd1',
severity: 'info',
code: 'SCAN_COMPLETE',
message: 'Scanned all files'
}]
};
console.log(JSON.stringify(output));// src/index.ts
import type { PluginInput, PluginOutput, PluginFinding } from '@quality-harness/code-to-gate/plugin';
function generateUUID(): string {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
const r = Math.random() * 16 | 0;
const v = c === 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
}
async function main(): Promise<void> {
// Read input from stdin
const inputJson = await readStdin();
const input: PluginInput = JSON.parse(inputJson);
const findings: PluginFinding[] = [];
// Implement your rule logic here
for (const file of (input.repo_graph as any).files || []) {
// Custom rule evaluation
}
// Write output to stdout
const output: PluginOutput = {
version: 'ctg.plugin-output/v1',
findings,
};
process.stdout.write(JSON.stringify(output));
}
function readStdin(): Promise<string> {
return new Promise((resolve) => {
let data = '';
process.stdin.on('data', chunk => data += chunk);
process.stdin.on('end', () => resolve(data));
});
}
main().catch(err => {
console.error(JSON.stringify({
version: 'ctg.plugin-output/v1',
errors: [{ code: 'INTERNAL_ERROR', message: err.message }]
}));
process.exit(10);
});- Writing to OSS core repository files
- Reading files outside allowed paths
- Including secrets/tokens in output artifacts
- Network access by default
- Modifying suppression settings
- Changing release readiness status
- Reading
NormalizedRepoGraph - Reading configured external results
- Returning findings/risk-seeds/invariant-seeds
- Writing to plugin work directory
- Reading manifest-allowed repo paths
| Code | Meaning |
|---|---|
| 0 | Success |
| 6 | Plugin failure |
| 7 | Schema validation failure |
| 62 | Timeout |
| 10 | Internal error |
| Failure | Handling |
|---|---|
| Manifest invalid | Plugin not loaded, exit code 6 |
| Process spawn failed | Retry once, then PLUGIN_FAILED |
| Timeout | Retry once, then PLUGIN_FAILED |
| Output schema invalid | Invalid output isolated, exit code 7 |
| Secret leak detected | Output rejected, needs_review |
# Create test input
echo '{"version":"ctg.plugin-input/v1","repo_graph":{"files":[]}}' > test-input.json
# Run your plugin
node ./dist/index.js < test-input.json
# Validate output
cat output.json | jq '.version' # Should be "ctg.plugin-output/v1"code-to-gate plugin-sandbox status
code-to-gate plugin-sandbox run ./my-plugin --input ./test-input.json --sandbox dockercode-to-gate plugin-sandbox run ./my-plugin --input ./test-input.json --sandbox dockerPlugins can receive configuration from the main config file:
# ctg.config.yaml
plugins:
- name: "./plugins/my-custom-rule"
enabled: true
config:
custom_threshold: 0.8Configuration is passed in the input JSON under config.
Before publishing or submitting plugins for review, build a local marketplace registry artifact:
code-to-gate plugin-marketplace --plugins ./plugins --out .qh
code-to-gate schema validate .qh/plugin-marketplace.jsonplugin-marketplace.json records rule, reporter, exporter, importer, and
language plugin manifests with validation status, capabilities, schema inputs
and outputs, sandbox permissions, and distribution metadata.
- Create a GitHub repository
- Add proper documentation
- Generate and validate
plugin-marketplace.json - Publish to npm (optional)
- Submit to code-to-gate plugin registry
- Place in local directory
- Use
visibility: privatein manifest - Generate a private
plugin-marketplace.jsonfor internal review - Reference via
file:prefix in config:
plugins:
- name: "file:../private-rules"
enabled: true
visibility: private- Keep output small: Limit findings to 1000 per plugin
- Evidence is required: Every finding must have at least one evidence
- Confidence matters: Use realistic confidence values
- Handle errors gracefully: Return errors in output, not as exit codes
- Don't leak secrets: Never include credentials in output
- Follow naming conventions: Use uppercase snake_case for rule IDs
- Document rules: Include clear titles and summaries
See docs/plugin-examples.md for complete examples of each plugin type.