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);
    }
}
No registration needed. 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.

PropertyTypeDescription
context.DiffDiffContextThe full diff, pre-filtered to eligible C# files
context.Diff.FilesIList<DiffFile>All changed files in the diff
context.EligibleFilesIReadOnlyList<...>File classification metadata (path, classification, source text)
context.StaticAnalysisAnalyzerResult?Roslyn diagnostics and symbol data (may be null)
context.SyntaxSyntaxContext?Roslyn syntax trees per file for AST-level analysis
context.TargetFrameworkstring?Target framework moniker detected from .csproj (e.g. net8.0)

DiffFile

PropertyDescription
file.NewPathFile path after the change
file.AddedLinesLines added in this diff (the + lines)
file.RemovedLinesLines removed in this diff (the - lines)
file.HunksAll 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);
ConfidenceMeaning
HighPattern is almost certainly a problem; reviewer should block
MediumLikely a problem; reviewer should verify before merging
LowPossible 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.

Next steps