Policy as code

Your code.
Your rules.

Your AI agent can forget instructions and conventions. CodeRuler never forgets the rules you save.

CHAOS OUT. CONVENTIONS IN.

How it works

Policy intent

When the output count is known, allocate the slice at its final length.

Steps

  1. Choose the files.

    The **/*.go pattern selects Go files, including internal/store/scores.go below.

  2. Select candidate functions.

    The selector finds functions and methods with a for loop that contains append. The AI checks each match.

  3. Check the selected function.

    The AI receives the selected function. It can compare the loop's output count with the slice length and decide whether to fill the slice directly.

Run from your repository root:

coderuler check
.coderuler.examples/go-append-slice.rule.ts
import { Rule, ai, scopes } from '@alex-dbs/coderuler';

export default class GoAppendSlice extends Rule {
  enabled = false;
  files = ['**/*.go'];
  scope = scopes.astGrep({
    language: 'go',
    rule: {
      any: [{ kind: 'function_declaration' }, { kind: 'method_declaration' }],
      has: {
        kind: 'for_statement',
        stopBy: 'end',
        has: {
          pattern: 'append($SLICE,$$$ARGS)',
          stopBy: 'end',
        },
      },
    },
  });

  check = ai({
    instruction: `For a fresh slice allocated for a loop with a known output count, allocate its final length and fill it by index or copy.
Keep append when the final count is unknown before the loop or when the slice has a deliberately assigned prefix.
Do not allocate elements that the loop then appends instead of filling them.`,
  });
}
Read the full source policy
Violation

go-append-slice

internal/store/scores.go:6

For this known-size loop, allocate the final length and fill by index.

package store

func growScores(scores []int) []int {
    out := make([]int, 0, len(scores))
    for _, score := range scores {
        out = append(out, score)
    }
    return out
}

From source to a decision

Rule workflow

Follow the rule on the left as its source moves through each step on the right.

Choose a check

Rule instruction

Check each Go pointer for nil on every path before dereferencing it. Check every nested pointer in the field path. This policy applies to explicit dereferences, not ordinary field access.

go-dereference-guard.rule.ts
import { Rule, ai, scopes, type ScopeSubject } from '@alex-dbs/coderuler';

export default class GoDereferenceGuard extends Rule {
  files = ['**/*.go'];
  exclude = ['vendor/**'];

  scope = scopes.astGrep({
    language: 'go',
    rule: {
      any: [{ kind: 'function_declaration' },
            { kind: 'method_declaration' }],
      has: {
        kind: 'unary_expression',
        stopBy: 'end',
        has: { field: 'operator', regex: '^\\*$' },
      },
    },
  });

  filter = (subject: ScopeSubject) =>
    !subject.filepath.endsWith('_test.go');

  check = ai({
    instruction: 'Check each Go pointer for nil on every path before dereferencing it. Check every nested pointer in the field path. This policy applies to explicit dereferences, not ordinary field access.',
    violationMessage: 'Check each pointer for nil before using *.',
  });
}
Read the full source policy

The AI checks each selected function for missing nil checks. It reads the Go file to find nested pointer types.

service-proto.rule.ts
import { Rule, regex, scopes, type ScopeSubject } from '@alex-dbs/coderuler';

export default class ServiceProto extends Rule {
  files = ['**/*.proto'];
  exclude = ['vendor/**'];

  scope = scopes.protobuf({
    path: "$.rpcs[*].options['google.api.http']..['get','put','post','delete','patch']",
  });

  filter = (subject: ScopeSubject) =>
    !subject.filepath.startsWith('proto/public/');

  check = regex({
    pattern: /^\/service\/v\d+\/[a-z][a-z0-9_]*s(?:\/.*)?$/,
    violationMessage: 'Use /service/vX/ and a lowercase plural resource.',
  });
}
Read the full source policy

Evaluate a regular expression locally against each selected HTTP URL. Public API files use a different URL policy, so the filter omits them.

source-line-limit.rule.ts
import { Rule, scopes, pass, violation, type ScopeSubject } from '@alex-dbs/coderuler';

export default class SourceLineLimit extends Rule {
  files = ['src/**/*.ts'];
  exclude = ['src/vendor/**'];

  scope = scopes.file();

  filter = (subject: ScopeSubject) =>
    !subject.filepath.endsWith('.test.ts');

  check: Rule['check'] = subject => {
    const lineCount = (subject as ScopeSubject).lines!.end;
    return lineCount > 80
      ? violation('Keep source files at 80 lines or fewer.')
      : pass();
  };
}
Open the custom-function docs

Run custom JavaScript or TypeScript logic without a provider. This check counts the lines in each selected source file.

  1. 01

    Choose files

    Include the paths you need. Exclude the rest.

    src/config.goIncluded →
    vendor/cache.goExcluded ↘
  2. 02

    Select portions

    A scope selects a whole file or a source fragment.

    package configfunc nestedValue(config *Config) int { if config == nil { return 0 } return *(*config).Child.Value }
    Selected contextnestedValue
  3. 03

    Filter matches

    Keep applicable matches. A filter does not decide pass or fail.

    trueconfig.go → check
    falseconfig_test.go → skip
  4. 04

    Run the check

    Evaluate the retained context against the rule instruction.

    func nestedValue(config *Config) int { if config == nil { return 0 } return *(*config).Child.Value }type Config struct { Child *Child } type Child struct { Value *int }
    Instruction sent to AI

    Check each Go pointer for nil on every path before dereferencing it. Check every nested pointer in the field path. This policy applies to explicit dereferences, not ordinary field access.

    Jev / AI decision model
    Violationviolation(…)

    Check each pointer for nil before dereferencing it.

Some ideas for you…

See how rules enforce policies in code and configuration.

Prevent unsafe HTTP responses.

131msto catch CWE-113: HTTP response splitting
at a cost of $0.0002

Violation

http-response-splitting

tests/manual/vulnerable_code.js:3

Reject CR and LF in attacker-controlled response headers and status messages before writing them.

export function respond(request, socket) {
  const name = new URL(request.url, 'http://localhost').searchParams.get('name');
  socket.write('HTTP/1.1 200 OK\r\nX-Name: ' + name + '\r\n\r\n');
}
http-response-splitting.rule.ts
import { Rule, ai, scopes } from '@alex-dbs/coderuler';

export default class HttpResponseSplitting extends Rule {
  files = ['tests/manual/vulnerable_code.js'];
  scope = scopes.astGrep({
    language: 'javascript',
    rule: {
      any: [
        { kind: 'function_declaration' },
        { kind: 'function_expression' },
        { kind: 'arrow_function' },
        { kind: 'method_definition' },
      ],
    },
  });

  check = ai({
    // Abridged prompt. Full policy and tests in the source.
    instruction: `Reject CR and LF in untrusted values before
inserting them into HTTP response headers or status lines.
This policy covers construction and writes visible in the selected
function. Hidden helper behavior and unseen callers are outside its
scope. Treat serialization arguments and statusMessage as untrusted.
Fixed values and values checked for both CR and LF meet this
requirement. Preserve fixed HTTP delimiters.`,
    violationMessage: 'Reject CR and LF in attacker-controlled response headers and status messages before writing them.',
  });
}
Read the full source policy

Ensure hexagonal architecture is respected.

This function queries SQL through a concrete *sql.DB client inside internal/app.

Call a port from the application layer. Keep the SQL query in the adapter.

Violation

go-app-no-infrastructure

internal/app/account/query.go:12

Move infrastructure I/O out of the domain and call an injected port instead.

package app

import (
    "context"
    "database/sql"
)

type Account struct { ID string }

func Find(ctx context.Context, db *sql.DB, id string) (Account, error) {
    var account Account
    err := db.QueryRowContext(ctx, "SELECT id FROM accounts WHERE id = ?", id).Scan(&account.ID)
    return account, err
}
go-app-no-infrastructure.rule.ts
import { Rule, ai, scopes } from '@alex-dbs/coderuler';

export default class GoAppNoInfrastructure extends Rule {
  enabled = false;
  files = ['internal/app/**/*.go'];
  scope = scopes.astGrep({
    language: 'go',
    rule: { any: [{ kind: 'function_declaration' }, { kind: 'method_declaration' }] },
  });

  check = ai({
    instruction: 'Keep each Go function in internal/app focused on application logic. Do not perform I/O with external systems through concrete clients in these functions; keep that work in adapters. A function may request external work through an injected interface (a port). Judge only the selected function.',
    violationMessage: 'Move infrastructure I/O out of the domain and call an injected port instead.',
  });
}
Read the full source policy

Go pointer dereferences need nil checks.

The code checks config for nil, but it does not check nested pointers Child or Value. This can cause nil pointer errors.

Violation

go-dereference-guard

internal/store/config.go:8

Check each pointer for nil before using *.

type Config struct { Child *Child }
type Child struct { Value *int }

func nestedValue(config *Config) int {
    if config == nil {
        return 0
    }
    return *(*config).Child.Value
}
go-dereference-guard.rule.ts
import { Rule, ai, scopes } from '@alex-dbs/coderuler';

export default class GoDereferenceGuard extends Rule {
  enabled = false;
  files = ['**/*.go'];

  scope = scopes.astGrep({
    language: 'go',
    rule: {
      any: [{ kind: 'function_declaration' }, { kind: 'method_declaration' }],
      has: {
        kind: 'unary_expression',
        stopBy: 'end',
        has: {
          field: 'operator',
          regex: '^\\*$',
        },
      },
    },
  });

  check = ai({
    instruction: `Check each Go pointer for nil before dereferencing it. For nested fields, check every pointer in the field path.`,
    violationMessage: 'Check each pointer for nil before using *.',
  });
}
Read the full source policy

Ensure conventions are followed.

This rule selects HTTP option values in internal Protobuf files. Each URL must use the /service/vN/ prefix. Optional subpaths are allowed.

The selected URL, /v1/invoices, is missing /service.

Violation

service-proto

tests/manual/proto/internal/service.proto:9

Service endpoints must start with a /service/vX/ .

syntax = "proto3";
import "google/api/annotations.proto";
package acme.billing.v1;
message Request {}
message Response {}
service BillingService {
  rpc ListInvoices(Request) returns (Response) {
    option (google.api.http) = {
      get: "/v1/invoices"
    };
  }
}
service-proto.rule.ts
import { Rule, regex, scopes, test } from '@alex-dbs/coderuler';

export default class ServiceProto extends Rule {
  enabled = false;
  files = ['tests/manual/proto/internal/*.proto'];

  scope = scopes.protobuf({ path: "$.rpcs[*].options['google.api.http']..['get','put','post','delete','patch','path']" });

  check = regex({
    pattern: /^\/service\/v\d+\/[a-z][a-z0-9_]*s(?:\/.*)?$/,
    violationMessage: 'Service endpoints must start with a /service/vX/ .',
  });
}
Read the full source policy

Validate JSON against a schema.

This rule uses plain code and the Ajv dependency to check each config file against a JSON Schema.

The value "8080" is a string, so it does not match the schema's integer requirement.

Violation

json-schema-config

config/app.json:2

/port: must be integer

{
  "port": "8080"
}
json-schema-config.rule.ts
import Ajv from 'ajv';
import { Rule, pass, scopes, test, violation } from '@alex-dbs/coderuler';
import type { Evaluator, ScopeSubject } from '@alex-dbs/coderuler';

const validateConfig = new Ajv().compile({
  type: 'object',
  properties: { port: { type: 'integer', minimum: 1, maximum: 65535 } },
  required: ['port'],
});

export default class JsonSchemaConfig extends Rule {
  enabled = false;
  files = ['config/**/*.json'];
  scope = scopes.json();

  check: Evaluator = subject => {
    const { data } = subject as ScopeSubject;
    if (validateConfig(data)) return pass();
    const error = validateConfig.errors?.[0];
    const path = error?.instancePath || '/';
    const message = error?.message ?? 'invalid value';
    return violation(`${path}: ${message}`);
  };
}
Read the full source policy

Get started

Run your rules
before review.

Give developers quick feedback on code issues before review. Let AI focus on the harder questions instead of checking every convention.

Search documentation

Type to search the documentation.

Search stays in your browser. Tab to browse · Enter to open