Custom Rules
Writing Custom Rules
GauntletCI is open source. All 35 built-in rules follow the same pattern. Adding a custom rule means implementing one interface, placing the file in one directory, and writing tests. No registration step is needed.
Two paths to custom detection
Code-based rule
Implement IRule in C#, place the file in src/GauntletCI.Core/Rules/Implementations/, run tests. Works for any pattern expressible against the diff or Roslyn AST. Best for precision and performance.
Engineering policy (no code)
Define team rules in a plain-text markdown file. GauntletCI evaluates diffs against them using a local LLM. Enable with experimental.engineeringPolicy in .gauntletci.json. See the configuration reference.
Step 1 - Choose an ID
Check docs/rules.md in the repository for the current ID registry. Pick the next unused GCI00XX slot. Never reuse a retired ID. Existing IDs are never renumbered - gaps in the sequence reflect rules that were retired or merged as the engine evolved.
Step 2 - Create the rule file
Create src/GauntletCI.Core/Rules/Implementations/GCI00XX_YourRuleName.cs. Extend RuleBase (which implements IRule) and override Id, Name, and EvaluateAsync.
// SPDX-License-Identifier: Elastic-2.0
using GauntletCI.Core.Analysis;
using GauntletCI.Core.Model;
namespace GauntletCI.Core.Rules.Implementations;
/// <summary>
/// GCI00XX - Your Rule Name
/// One-sentence description of what this rule detects.
/// </summary>
public class GCI00XX_YourRuleName : RuleBase
{
public override string Id => "GCI00XX";
public override string Name => "Your Rule Name";
public override Task<List<Finding>> EvaluateAsync(
AnalysisContext context, CancellationToken ct = default)
{
var findings = new List<Finding>();
foreach (var file in context.Diff.Files)
{
foreach (var line in file.AddedLines)
{
if (!line.Content.Contains("YOUR_PATTERN", StringComparison.Ordinal))
continue;
findings.Add(CreateFinding(
file: file,
line: line,
summary: "Short description of what was found.",
evidence: line.Content.Trim(),
whyItMatters: "Why this pattern is risky in production.",
suggestedAction: "Concrete step the developer can take to fix it.",
confidence: Confidence.Medium));
}
}
return Task.FromResult(findings);
}
}RuleOrchestrator.CreateDefault() discovers all IRule implementations in the assembly via reflection. Drop the file and it runs.Step 3 - Key APIs
AnalysisContext
Passed to every rule. Contains the filtered diff and optional static analysis results.
| Property | Type | Description |
|---|---|---|
| context.Diff | DiffContext | The full diff, pre-filtered to eligible C# files |
| context.Diff.Files | IList<DiffFile> | All changed files in the diff |
| context.EligibleFiles | IReadOnlyList<...> | File classification metadata (path, classification, source text) |
| context.StaticAnalysis | AnalyzerResult? | Roslyn diagnostics and symbol data (may be null) |
| context.Syntax | SyntaxContext? | Roslyn syntax trees per file for AST-level analysis |
| context.TargetFramework | string? | Target framework moniker detected from .csproj (e.g. net8.0) |
DiffFile
| Property | Description |
|---|---|
| file.NewPath | File path after the change |
| file.AddedLines | Lines added in this diff (the + lines) |
| file.RemovedLines | Lines removed in this diff (the - lines) |
| file.Hunks | All diff hunks; each hunk has .Lines with +/- and context lines |
CreateFinding() overloads
// Diff-wide finding (no file/line attribution) CreateFinding(summary, evidence, whyItMatters, suggestedAction, confidence); // File-level finding CreateFinding(file, summary, evidence, whyItMatters, suggestedAction, confidence); // Line-level finding (most precise) CreateFinding(file, summary, evidence, whyItMatters, suggestedAction, confidence, line);
| Confidence | Meaning |
|---|---|
| High | Pattern is almost certainly a problem; reviewer should block |
| Medium | Likely a problem; reviewer should verify before merging |
| Low | Possible concern; reviewer should be aware |
Step 4 - Configurable rules (optional)
If your rule needs access to .gauntletci.json values at evaluation time, implement IConfigurableRule. The orchestrator calls Configure() once after discovery.
public class GCI00XX_YourRuleName : RuleBase, IConfigurableRule
{
private GauntletConfig _config = new();
public void Configure(GauntletConfig config) => _config = config;
public override Task<List<Finding>> EvaluateAsync(
AnalysisContext context, CancellationToken ct = default)
{
// Access _config.Rules["GCI00XX"], _config.ForbiddenImports, etc.
}
}See GCI0035_ArchitectureLayerGuard.cs for a complete example using ForbiddenImports from config.
Step 5 - Write tests
Create src/GauntletCI.Tests/Rules/GCI00XXTests.cs. Cover at minimum: one true positive, one false positive, and one edge case (empty diff, test-file exclusion, etc.).
public class GCI00XXTests
{
private static Task<List<Finding>> Run(string rawDiff)
{
var rule = new GCI00XX_YourRuleName();
var diff = DiffParser.Parse(rawDiff);
var context = new AnalysisContext { Diff = diff };
return rule.EvaluateAsync(context);
}
[Fact]
public async Task TruePositive_PatternPresent_ShouldFlag()
{
var findings = await Run("""
diff --git a/src/MyFile.cs b/src/MyFile.cs
index abc..def 100644
--- a/src/MyFile.cs
+++ b/src/MyFile.cs
@@ -1,3 +1,4 @@
namespace MyApp;
+YOUR_PATTERN_HERE
""");
Assert.Single(findings);
Assert.Equal("GCI00XX", findings[0].RuleId);
}
[Fact]
public async Task FalsePositive_SafePattern_ShouldNotFlag()
{
var findings = await Run("""
diff --git a/src/MyFile.cs b/src/MyFile.cs
index abc..def 100644
--- a/src/MyFile.cs
+++ b/src/MyFile.cs
@@ -1,3 +1,4 @@
namespace MyApp;
+SAFE_EQUIVALENT_HERE
""");
Assert.Empty(findings);
}
}Rule inclusion criteria
GauntletCI rules detect behavioral risk in diffs, not style. A rule is eligible for inclusion if it meets all of the following criteria from CHARTER.md:
- +The pattern has caused production incidents in real systems
- +It is detectable from the diff alone (no runtime information required)
- +It produces a low false-positive rate on typical PRs
- +The finding is actionable - a developer can fix it immediately
Rules that check formatting, naming conventions, or code style are not eligible.
Contributing your rule
Once your rule has tests and passes the inclusion criteria, open a pull request. See the CONTRIBUTING.md for the full workflow, commit conventions, and step-by-step rule writing guide.
