Configuration
Codeward is configured with a single file in your repository root. Both JSON and YAML formats are fully supported.
Don't want to write configs from scratch? Browse the Codeward Registry for ready-to-use policy profiles covering security, infrastructure, language-specific checks, compliance, and more.
Config File Discovery
Codeward looks for configuration in this order (first found wins):
- Path specified by
CODEWARD_CONFIG_PATH(or--configCLI flag) .codeward.json.codeward.yaml.codeward.yml- Built-in defaults (if no config found)
Top-Level Structure
# .codeward.yaml
global:
dependency_tree: false
logging: # log level, format, destination (optional)
summary: # run-summary verbosity (optional)
vulnerability: # vulnerability policies
license: # license policies
package: # package policies
file: # file validation policies
pr: # PR validation policies
sbom: # SBOM export settings (optional)
All policy arrays are optional. Omit any you don't need.
Global Settings
Everything below lives under the top-level global key.
| Setting | Type | Default | Description |
|---|---|---|---|
dependency_tree | boolean | false | Enable dependency graph analysis. Adds Relationship, Parents, Children, Sources fields. |
license_conflicts | boolean | false | Detect OSADL-derived incompatibilities between licences in the tree. |
dependency.install | boolean | false | Install dependencies before scanning, to enrich licence detection. |
dependency.install_dev | boolean | false | Include dev dependencies when installing. |
main_path | string | see below | Base checkout to scan. Overrides the built-in path for the current mode. |
branch_path | string | see below | Head checkout to scan in diff mode. |
ignore | array | [] | Global ignore rules. See Ignore Rules. |
global.modeThe scan mode comes from CODEWARD_MODE or --mode only. A mode: key under
global is silently discarded — unknown config keys are dropped, not rejected.
Logging is configured at the top level, not under global; see
Logging.
Scan paths
main_path and branch_path default to whatever the private config for the
current invocation says, and that depends on whether CODEWARD_MODE is set:
CODEWARD_MODE | main_path | branch_path |
|---|---|---|
| unset | ./ (current directory) | — |
main | /main | — |
diff | /main | /branch |
The /main and /branch defaults exist for the Docker image's bind mounts.
Running the binary directly with CODEWARD_MODE set, you must also set
CODEWARD_MAIN_PATH (and CODEWARD_BRANCH_PATH in diff mode) or the scan
looks for directories that do not exist. See
Standalone Binary.
Execution Mode
The CODEWARD_MODE environment variable (or --mode) controls how Codeward scans.
| Mode | When to Use | Behavior |
|---|---|---|
diff | Pull requests | Compares feature vs main branch. Categorizes as new, changed, removed, existing. Enables git:pr. |
main | Push to main, scheduled scans | Scans main branch only. All findings are existing. Enables git:issue. |
Key insight: Only new findings should typically block PRs. Existing technical debt doesn't slow you down.
Setting the Mode
# Via environment variable (recommended for CI)
export CODEWARD_MODE=diff
# Via CLI flag
codeward-scan --mode diff
CLI flags > environment variables > config file.
Policy Structure
Every policy follows this structure:
name: policy-name
disabled: false
actions:
new: block
existing: warn
removed: info
changed: warn
operator: or # or, and, implies
rules:
- { field: Severity, type: eq, value: CRITICAL }
outputs:
- format: markdown
destination: git:pr
| Field | Required | Description |
|---|---|---|
name | Yes | Unique identifier for the policy |
disabled | No | Set true to skip this policy |
actions | Yes | What to do for each change category |
operator | No | or (default), and, or implies for rule matching |
rules | Yes | Array of filter rules (can be empty for "match all") |
outputs | Yes | Where and how to report results |
Policy Validation
Codeward validates your configuration. If a policy has errors:
- The invalid policy is skipped with a warning
- Valid policies continue executing
- Scan only fails if all policies are invalid
This ensures a typo in one policy doesn't break your entire CI pipeline.
Actions
| Action | Effect | Exit Code |
|---|---|---|
info | Log only, no impact on CI | 0 |
warn | Visible in output, CI passes | 0 |
block | CI fails (non-zero exit code) | 1 |
ignore | Suppress from output entirely | 0 |
File and PR policies use only the existing action since they don't track changes over time. Config validation will warn if new, removed, or changed actions are specified.
Change Categories
| Category | Meaning | Typical Action |
|---|---|---|
new | Only in feature branch (introduced) | block or warn |
changed | In both, but attributes differ | warn |
removed | Only in main branch (deleted) | info |
existing | In both, unchanged | info or warn |
Rules
Rules define what to search for. When a rule matches, it creates a finding that triggers the configured action.
Rule Types
| Type | Reports When | Example |
|---|---|---|
eq | Value equals target | { field: Severity, type: eq, value: CRITICAL } |
ne | Value does not equal target | { field: Severity, type: ne, value: LOW } |
lt / gt | Less / greater than (semver-aware) | { field: Score, type: gt, value: "7.0" } |
le / ge | Less/greater or equal (semver-aware) | { key: changed_files, type: ge, value: "20" } |
contains | Value contains substring | { field: Title, type: contains, value: injection } |
not_contains | Value does not contain substring | { field: PkgName, type: not_contains, value: test } |
hasPrefix | Value starts with string | { field: VulnerabilityID, type: hasPrefix, value: CVE-2024 } |
hasSuffix | Value ends with string | { field: PkgName, type: hasSuffix, value: -dev } |
regex | Value matches regex | { field: Description, type: regex, value: "remote.*code" } |
not_regex | Value does not match regex | { key: title, type: not_regex, value: "^(feat|fix|docs):" } |
in | Value is in comma-separated set | { field: Severity, type: in, value: "CRITICAL,HIGH" } |
not_in | Value is not in set | { field: Category, type: not_in, value: "notice,permissive" } |
exists | Key or file exists | { type: exists, key: scripts.test } |
not_exists | Key or file does not exist | { type: not_exists, path: ".env.local" } |
last_match | Last line matching filter matches value | See Last Match Rules |
not_last_match | Last line matching filter does NOT match value | See Last Match Rules |
Go's regexp uses RE2 which does not support lookahead/lookbehind. Use not_regex instead of (?!...). See RE2 Reference.
Rule Field Naming
| Policy Type | Property | Description |
|---|---|---|
| vulnerability / license / package | field | Entity field name (e.g., Severity, PkgName) |
| file / pr | key (or field alias) | Key path (e.g., version, dependencies.axios) |
Field names are case-insensitive for PR policies.
Policy Operators
OR (default) — match if any rule matches:
operator: or
rules:
- { field: Severity, type: eq, value: CRITICAL }
- { field: Severity, type: eq, value: HIGH }
AND — match only if all rules match:
operator: and
rules:
- { field: Severity, type: eq, value: CRITICAL }
- { field: PkgName, type: eq, value: lodash }
implies — conditional "If X then Y" logic:
operator: implies
rules:
- { key: changed_files, type: gt, value: "10" } # trigger
- { key: "files.filename", file_pattern: "docs/**", type: not_exists } # condition
- First rule is the trigger — if it doesn't match, policy passes silently
- Remaining rules are conditions — evaluated only if trigger matches
- Use
RuleRolefield in outputs to show trigger/condition labels
Output Configuration
outputs:
- format: markdown
destination: git:pr
template: table
fields: [VulnerabilityID, PkgName, Severity, FixedVersion]
changes: [new, changed]
group_by: [Severity]
combined: false
collapse: true
title: Security Issues
comment: Please fix before merging
| Field | Required | Description |
|---|---|---|
format | Yes | markdown, html, json, or sarif |
destination | Yes | Where to send output (see Outputs) |
template | No | table, text, or combined (not for JSON) |
fields | No | Which fields to include (defaults per policy type) |
changes | No | Filter to change categories |
group_by | No | Group results by these fields |
combined | No | Merge with other outputs at same destination |
collapse | No | Make section collapsible (markdown/html) |
title | No | Custom section title |
comment | No | Text shown below title |
webhook | No | Custom webhook config (only for url: destinations) |
See Outputs for full details.
Ignore Rules
Global Ignores
Filter findings by the source they came from (the lockfile or file path) and by target (the CVE ID, package name, or licence the finding is about), across all policies:
global:
ignore:
- name: Ignore test directories
paths: ["test/", "_test.go"]
- name: Ignore specific CVEs
targets: [CVE-2023-1234, CVE-2023-5678]
| Field | Description |
|---|---|
name | Name for logging (required) |
paths | Substrings matched against the finding's source path |
targets | Items to ignore, matched exactly (CVE IDs, package names, etc.) |
paths are substrings, not globsEach entry becomes a source contains <value> rule. A glob such as
"**/*_test.go" is matched literally and therefore never fires — write
"_test.go" instead. Likewise "test/**" should be "test/".
paths and targets combine with OR, and there is no expiresEvery paths and targets entry in one rule is OR-ed together: a finding
matching any of them is ignored, not one matching all of them. Split them
into separate rules if you meant either on its own, and use a
policy-level ignore with operator: and when you
genuinely need "this CVE, but only under vendor/".
Global ignores also have no expires field. An expires: key here is silently
discarded, so the rule never stops applying. Expiry is supported on
policy-level ignores only.
Policy-Level Ignores
Content-based ignores within a specific policy. The key is ignore (singular):
vulnerability:
- name: Critical vulnerabilities
actions: { new: block }
rules:
- { field: Severity, type: eq, value: CRITICAL }
ignore:
- name: Windows-only CVE
expires: "2026-06-30"
author: security-[email protected]
operator: and
rules:
- { field: VulnerabilityID, type: eq, value: CVE-2024-0001 }
- { field: PkgName, type: eq, value: openssl }
| Field | Description |
|---|---|
name | Name for logging |
description | Free text |
author | Who accepted the risk |
operator | or (default) or and |
expires | Expiration date YYYY-MM-DD |
rules | Filter rules using same syntax as policy rules |
ignores: does nothingUnknown config keys are dropped without an error, so a misspelled ignores:
block leaves the policy blocking exactly as before. If an allow-list appears to
have no effect, check the spelling first.
Logging
logging and summary are top-level keys, siblings of global — not
nested under it:
logging:
level: INFO # DEBUG, INFO, WARN, ERROR
format: text # text or json
timestamp: false # include timestamps
output: stderr # stderr or stdout
summary:
level: standard # none, minimal, standard, detailed
Environment variables override config: CODEWARD_LOG_LEVEL, CODEWARD_LOG_FORMAT, CODEWARD_LOG_TIMESTAMP, CODEWARD_LOG_OUTPUT, CODEWARD_LOG_SUMMARY.
Default Configuration
When no config file exists, Codeward falls back to a built-in configuration:
- Vulnerabilities:
warnon new/changed/existing CRITICAL and HIGH,infoon removed. Posts new/removed/changed to the PR; opens an issue for existing. - Licenses:
warnon new/changed/existing,infoon removed. Posts to PR + creates issues. - Packages:
infofor all changes. Posts to PR. - File/PR Validation: Not enabled by default.
Nothing in the built-in config uses the block action, so it never fails a
build on its own.
Its rules filter on Category values Forbidden and Restricted, but
categories are lowercase and matched exactly (see
License categories). Only its third rule,
Severity eq UNKNOWN, can fire. Define your own licence policy with
forbidden / restricted rather than relying on the default.
Starter Configurations
Block Critical Only
vulnerability:
- name: block-critical
actions: { new: block }
rules:
- { field: Severity, type: eq, value: CRITICAL }
outputs:
- format: markdown
destination: git:pr
fields: [VulnerabilityID, PkgName, Severity, FixedVersion]
changes: [new]
Full: Vulnerabilities + Licenses + Packages
vulnerability:
- name: crit-high-block
actions: { new: block, existing: warn }
rules:
- { field: Severity, type: eq, value: CRITICAL }
- { field: Severity, type: eq, value: HIGH }
outputs:
- format: markdown
destination: git:pr
fields: [VulnerabilityID, PkgName, Severity, FixedVersion]
changes: [new]
license:
- name: gpl-block
actions: { new: block }
rules:
- { field: Category, type: eq, value: restricted }
outputs:
- format: markdown
destination: git:pr
fields: [Name, Category, Severity, PkgName]
changes: [new]
package:
- name: new-packages
actions: { new: info }
rules: []
outputs:
- format: markdown
destination: git:pr
fields: [Name, Version]
changes: [new]
Progressive Enforcement
| Phase | Approach |
|---|---|
| 1. Observe | All actions set to info |
| 2. Warn | Change new to warn |
| 3. Block Critical | Set new: block for CRITICAL only |
| 4. Expand | Add HIGH to blocking |
| 5. Steady State | Block what matters, warn on the rest |
Tip: Use separate policies for different severity levels so you can promote them independently.