Skip to main content
Version: Latest

Configuration

Codeward is configured with a single file in your repository root. Both JSON and YAML formats are fully supported.

Ready-Made Profiles

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):

  1. Path specified by CODEWARD_CONFIG_PATH (or --config CLI flag)
  2. .codeward.json
  3. .codeward.yaml
  4. .codeward.yml
  5. 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.

SettingTypeDefaultDescription
dependency_treebooleanfalseEnable dependency graph analysis. Adds Relationship, Parents, Children, Sources fields.
license_conflictsbooleanfalseDetect OSADL-derived incompatibilities between licences in the tree.
dependency.installbooleanfalseInstall dependencies before scanning, to enrich licence detection.
dependency.install_devbooleanfalseInclude dev dependencies when installing.
main_pathstringsee belowBase checkout to scan. Overrides the built-in path for the current mode.
branch_pathstringsee belowHead checkout to scan in diff mode.
ignorearray[]Global ignore rules. See Ignore Rules.
There is no global.mode

The 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_MODEmain_pathbranch_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.

ModeWhen to UseBehavior
diffPull requestsCompares feature vs main branch. Categorizes as new, changed, removed, existing. Enables git:pr.
mainPush to main, scheduled scansScans 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
FieldRequiredDescription
nameYesUnique identifier for the policy
disabledNoSet true to skip this policy
actionsYesWhat to do for each change category
operatorNoor (default), and, or implies for rule matching
rulesYesArray of filter rules (can be empty for "match all")
outputsYesWhere and how to report results

Policy Validation

Codeward validates your configuration. If a policy has errors:

  1. The invalid policy is skipped with a warning
  2. Valid policies continue executing
  3. Scan only fails if all policies are invalid

This ensures a typo in one policy doesn't break your entire CI pipeline.

Actions

ActionEffectExit Code
infoLog only, no impact on CI0
warnVisible in output, CI passes0
blockCI fails (non-zero exit code)1
ignoreSuppress from output entirely0
File/PR Validation

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

CategoryMeaningTypical Action
newOnly in feature branch (introduced)block or warn
changedIn both, but attributes differwarn
removedOnly in main branch (deleted)info
existingIn both, unchangedinfo or warn

Rules

Rules define what to search for. When a rule matches, it creates a finding that triggers the configured action.

Rule Types

TypeReports WhenExample
eqValue equals target{ field: Severity, type: eq, value: CRITICAL }
neValue does not equal target{ field: Severity, type: ne, value: LOW }
lt / gtLess / greater than (semver-aware){ field: Score, type: gt, value: "7.0" }
le / geLess/greater or equal (semver-aware){ key: changed_files, type: ge, value: "20" }
containsValue contains substring{ field: Title, type: contains, value: injection }
not_containsValue does not contain substring{ field: PkgName, type: not_contains, value: test }
hasPrefixValue starts with string{ field: VulnerabilityID, type: hasPrefix, value: CVE-2024 }
hasSuffixValue ends with string{ field: PkgName, type: hasSuffix, value: -dev }
regexValue matches regex{ field: Description, type: regex, value: "remote.*code" }
not_regexValue does not match regex{ key: title, type: not_regex, value: "^(feat|fix|docs):" }
inValue is in comma-separated set{ field: Severity, type: in, value: "CRITICAL,HIGH" }
not_inValue is not in set{ field: Category, type: not_in, value: "notice,permissive" }
existsKey or file exists{ type: exists, key: scripts.test }
not_existsKey or file does not exist{ type: not_exists, path: ".env.local" }
last_matchLast line matching filter matches valueSee Last Match Rules
not_last_matchLast line matching filter does NOT match valueSee Last Match Rules
RE2 Regex

Go's regexp uses RE2 which does not support lookahead/lookbehind. Use not_regex instead of (?!...). See RE2 Reference.

Rule Field Naming

Policy TypePropertyDescription
vulnerability / license / packagefieldEntity field name (e.g., Severity, PkgName)
file / prkey (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 RuleRole field 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
FieldRequiredDescription
formatYesmarkdown, html, json, or sarif
destinationYesWhere to send output (see Outputs)
templateNotable, text, or combined (not for JSON)
fieldsNoWhich fields to include (defaults per policy type)
changesNoFilter to change categories
group_byNoGroup results by these fields
combinedNoMerge with other outputs at same destination
collapseNoMake section collapsible (markdown/html)
titleNoCustom section title
commentNoText shown below title
webhookNoCustom 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]
FieldDescription
nameName for logging (required)
pathsSubstrings matched against the finding's source path
targetsItems to ignore, matched exactly (CVE IDs, package names, etc.)
paths are substrings, not globs

Each 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 expires

Every 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 }
FieldDescription
nameName for logging
descriptionFree text
authorWho accepted the risk
operatoror (default) or and
expiresExpiration date YYYY-MM-DD
rulesFilter rules using same syntax as policy rules
ignores: does nothing

Unknown 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: warn on new/changed/existing CRITICAL and HIGH, info on removed. Posts new/removed/changed to the PR; opens an issue for existing.
  • Licenses: warn on new/changed/existing, info on removed. Posts to PR + creates issues.
  • Packages: info for 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.

The built-in licence policy currently matches nothing

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

PhaseApproach
1. ObserveAll actions set to info
2. WarnChange new to warn
3. Block CriticalSet new: block for CRITICAL only
4. ExpandAdd HIGH to blocking
5. Steady StateBlock what matters, warn on the rest

Tip: Use separate policies for different severity levels so you can promote them independently.