Project configuration
With a .codeguard.yml you adapt CodeGuard to your project: paths, platforms, rules, metrics, custom rules and waivers. This page describes every key.
Where the file lives and how it is found
| Invocation | What is loaded | In the report (project_source.mode) |
|---|---|---|
| no option, file present | <project root>/.codeguard.yml or .codeguard.yaml | auto |
| no option, no file | defaults | field missing |
--config X | only X | explicit |
--no-project-config | defaults, warning codeguard-project-config-ignored if a file exists | disabled |
--config and --no-project-config | nothing | exit 2 |
- The project root is
--project-rootor the current directory. CodeGuard does not search parent directories or the Git root. - Only these exact file names count, even on case-insensitive volumes.
.CodeGuard.ymlis not loaded. - If both
.codeguard.ymland.codeguard.yamlare in the root directory, that is exit 2 (ambiguous). - A broken file is exit 2. CodeGuard never silently falls back to defaults.
.codeguard.ymland.codeguard.yamlare protected files by default. A diff that changes them ends with exit 6.
The smallest valid file:
version: 1
codeguard schema --kind configuration returns the full schema. You can validate the file at any time with:
codeguard config validate
Levels and precedence
The effective configuration is always the result of three sources, in this order:
- Built-in defaults
- Organization policy (
/Library/Application Support/CodeGuard/policy.yml): metric baseline, mandatory rules, severity floors, locked values, trusted tool directories. See Organization policy. - Project configuration (
.codeguard.yml)
The policy then enforces its locked values (locked_values). A project value that contradicts a locked value is exit 2.
codeguard 0.3.5 doesn't load user or CI configuration files. --ci doesn't load an additional file either.
If the project configuration sets a list (e.g. paths.exclude or protected_files), it replaces the default list. If you want to keep the default entries, include them in your list.
What the project level may do
The project level may tighten settings, but not loosen any security guarantee. These limits were verified with codeguard 0.3.5:
| Project sets … | Result |
|---|---|
project.*, paths.*, protected_files, rules.*, custom_rules, waivers, diff.* | allowed |
checks.*.scopes, checks.platform.*, checks.tests.simulator, checks.tests.ipad | allowed |
checks.<format/swiftlint/compile/tests/security>.enabled: false or .required: false | exit 2: required checks cannot be disabled by a lower-precedence source |
checks.compile.enabled: false | exit 2: tests require compile |
execution.analysis_limits.* | allowed |
execution.timeouts.* | exit 2, unless the value repeats the default |
execution.max_workers and other execution.* | exit 2, unless the value repeats the default |
security.* (including security.network.allowed_destinations) | exit 2, unless the value repeats the default |
tools.* | exit 2 (configuration source is not allowed to set this field), unless the policy allows it with allow_project_tool_paths: true |
logging.level other than info | exit 2: project configuration may only repeat the safe logging default |
logging.audit.enabled: false, logging.audit.include_tool_raw_output: true | exit 2 |
| a value the organization has locked, set differently | exit 2 |
Whatever the project level may not do, the organization policy can set via enforcement.locked_values.
version
| Key | Type | Default | Required |
|---|---|---|---|
version | integer, only 1 | 1 | yes |
Keys starting with x- are allowed as extensions at every level and are ignored. Any other unknown key is exit 2.
project
Describes how CodeGuard builds and tests the project.
| Key | Values | Default | Effect |
|---|---|---|---|
project.kind | auto, swift-package, xcode | auto | Choice of build tool (see below) |
project.package_path | path or null | null | Names the manifest. Only the Package.swift in the root directory is accepted; a package in a subfolder is exit 2. |
project.xcode.workspace | path to .xcworkspace or null | null | Container for Xcode builds |
project.xcode.project | path to .xcodeproj or null | null | Container for Xcode builds |
project.xcode.scheme | name or null | null | Scheme. If not set, CodeGuard uses the project's shared scheme; without a shared scheme, exit 2. |
project.xcode.configuration | name | Debug | Build configuration for xcodebuild |
project.xcode.destination | platform=macOS or platform=macOS,arch=<host architecture> | null | Deprecated. Now only a safeguard for macOS; any other value is exit 2. Use project.platforms. |
project.platforms | list of macos, ios, watchos, tvos, visionos | not set (automatic) | Platforms for compile/tests |
project.test_destinations | object (see below) | not set | Override simulator device and runtime per destination |
project.kind:
auto: If there is aPackage.swiftin the root directory, SwiftPM builds, even if an Xcode project exists as well. Otherwise an.xcodeproj/.xcworkspaceis built withxcodebuild(workspace before project). If neither exists,compileandtestsdon't run.swift-package: always SwiftPM, never Xcode.xcode: always Xcode.
project.platforms:
- Not set: CodeGuard builds every platform it detects and whose SDK is installed locally.
- Set: intersection of the list and the detected platforms.
- For Xcode projects, a platform the scheme doesn't support is exit 2.
- A Swift package may name any platform, even one its manifest doesn't list under
platforms:. This way[macos, ios]also builds an iOS package on macOS. - If the SDK of a listed platform is missing locally, the run ends with exit 3 before the first build.
- The list must not be empty and must not contain duplicates.
project.test_destinations: allowed keys ios, ipad, watchos, tvos, visionos. Each entry has:
| Field | Values | Meaning |
|---|---|---|
device_type | name or identifier of the device type, not empty | replaces the automatically chosen device type |
runtime | X.Y or latest | X.Y selects exactly this version (18.0 doesn't match 18.2) |
ios only applies to the iPhone destination, ipad only to the iPad destination. If a destination listed here can't be satisfied (runtime missing, runtime below the deployment target), the run ends with exit 3 before a device is created. An empty object, an unknown key or an invalid version format is exit 2.
project:
platforms: [ios]
test_destinations:
ios: { device_type: "iPhone 18 Pro", runtime: "27.0" }
ipad: { runtime: latest }
paths
| Key | Default | Effect |
|---|---|---|
paths.include | ["Sources/**", "Tests/**", "Package.swift"] | Only these files may be checked. Others result in path-outside-allowed-scope (exit 6) in check-file/check-diff. |
paths.exclude | [".build/**", "DerivedData/**", "Vendor/**", ".git/**"] | These files are excluded, also from the content rules and from reading by the platform checks |
paths.generated | [".codeguard-artifacts/**"] | Validated, but has no effect on the run in 0.3.5 |
Globs are relative to the project: ** (any number of levels), *, ?, [...]. Not allowed are a leading /, .. and the characters ! ~ ; | & < > ( ) { } $ as well as backtick and backslash.
The default only covers Sources/, Tests/ and Package.swift. If your sources live in, say, MyApp/ and MyAppTests/, you have to adjust paths.include; otherwise format, swiftlint and the content rules don't check these files. The platform checks read the project file, Info.plist and entitlements even outside paths.include.
To exclude a CocoaPods project, add Pods/** to paths.exclude. If an exclude covers a project's project.pbxproj, the platform checks silently skip that project.
protected_files
A list of globs. If a diff changes a matching file, the run ends with protected-file-change and exit 6. The default list is under Checks and rules. The organization policy can enforce entries with mandatory_protected_files that the project can't remove.
protected_files:
- .codeguard.yml
- .codeguard.yaml
- Package.swift
- Package.resolved
- "**/*.entitlements"
- "**/*.xcodeproj/project.pbxproj"
- .github/workflows/**
- .gitlab-ci.yml
- "**/*.xcconfig"
- Config/Secrets.plist # custom entry
checks
Each check has three fields:
| Field | Meaning |
|---|---|
enabled | Check is enabled |
required | mandatory: a missing tool or an incomplete check results in exit 3 instead of a warning |
scopes | In which commands the check runs: file (check-file), diff (check-diff), project (check-project), operation |
| Check | enabled | required | scopes (default) |
|---|---|---|---|
checks.security | true | true | [file, diff, project, operation] |
checks.platform | true | false | [file, diff, project] |
checks.format | true | true | [file, diff, project] |
checks.swiftlint | true | true | [file, diff, project] |
checks.compile | true | true | [diff, project] |
checks.tests | true | true | [diff, project] |
Additionally for checks.tests:
| Field | Default | Effect |
|---|---|---|
simulator | true | false skips all simulator test destinations (simulatorDisabled) |
ipad | true | false prevents the additional iPad run |
What the project can do with checks:
- Restricting
scopesis allowed, even to[]. This removes a check from every run without disabling it.compileandtestsnever run infileanyway. checks.platformmay be disabled entirely by the project (enabled: false) or made mandatory (required: true).enabled: falseorrequired: falseforformat,swiftlint,compile,testsandsecurityis exit 2. Only the organization policy may do this vialocked_values.
Example: static checks only, no build and no tests:
version: 1
checks:
compile: { scopes: [] }
tests: { scopes: [] }
rules
Every built-in rule under rules.<id> supports these fields:
| Field | Values | Meaning |
|---|---|---|
enabled | true/false | Rule on or off |
severity | info, warning, error, critical | Severity. error and critical block. |
scopes | file, diff, project, operation | in which commands the rule runs |
path_overrides | list of { paths, enabled?, severity? } | different settings for specific paths |
Rule IDs this applies to:
- Content rules:
forbidden-fatal-error,forbidden-force-try,forbidden-force-cast,sensitive-data-in-log,hardcoded-secret,unauthorized-network-destination - Platform rules:
privacy-manifest-invalid,privacy-manifest-missing,privacy-required-reason-undeclared,privacy-reason-code-invalid,privacy-tracking-domains-missing,purpose-string-missing,purpose-string-empty,ats-arbitrary-loads,ats-insecure-exception-domain,entitlements-file-missing,entitlement-weakens-hardened-runtime,entitlement-get-task-allow-release,macos-app-sandbox-missing
The defaults are listed under Checks and rules. The keys rules.public-api-change and rules.dependency-policy exist in the schema but have no effect in 0.3.5.
Additional fields:
| Key | Default | Meaning |
|---|---|---|
rules.sensitive-data-in-log.logger_symbols.functions | [print, debugPrint, dump, NSLog, os_log] | free logger functions |
rules.sensitive-data-in-log.logger_symbols.methods | [debug, info, notice, log, trace, warning, error, fault, critical] | logger methods (the receiver must contain log) |
rules.sensitive-data-in-log.sensitive_identifiers | [token, accessToken, refreshToken, password, passwd, secret, apiKey, authorization, cookie, sessionID, privateKey] | sensitive identifiers (also used by the assignment heuristic of hardcoded-secret) |
rules.hardcoded-secret.allowlist_sha256 | [] | allowlist specific values as sha256:<64 hex> (format tier and URL userinfo only; for URLs, the hash of the password) |
The project may not go below a minimum severity that the organization sets via severity_floors (exit 2). It may not disable mandatory rules (mandatory_rules).
Example:
rules:
forbidden-fatal-error:
severity: critical
path_overrides:
- paths: ["Tests/**", "Sources/Previews/**"]
enabled: false
sensitive-data-in-log:
sensitive_identifiers: [token, password, iban]
hardcoded-secret:
path_overrides:
- paths: ["Tests/Fixtures/**"]
enabled: false
ats-arbitrary-loads:
severity: error
For forbidden-*, a path_overrides entry replaces the default overrides for test paths, because lists are replaced. Add Tests/**, **/*Tests/** and **/*UITests/** again if the rule should stay off there.
rules.metrics
Configures the managed SwiftLint metric rules. Without rules.metrics, there is no metrics run. Keys here are snake_case.
| Key | Parameters |
|---|---|
severity | error (default) or critical: severity of a finding above the error threshold |
line_length | warning, error, ignores_urls, ignores_function_declarations, ignores_comments, ignores_interpolated_strings |
file_length | warning, error, ignore_comment_only_lines |
type_body_length | warning, error |
function_body_length | warning, error |
closure_body_length | warning, error |
cyclomatic_complexity | warning, error, ignores_case_statements |
nesting | type_level.warning, type_level.error, function_level.warning, function_level.error |
function_parameter_count | warning, error, ignores_default_parameters |
large_tuple | warning, error |
enum_case_associated_values_count | warning, error |
Rules for the values:
-
Thresholds are integers ≥ 1, and
warning ≤ error. -
Every rule in the project needs at least one threshold. A switch alone is exit 2:
configuration validation failed: project:/rules/metrics/line_length: metric rule requires at least one warning or error threshold -
Only configured thresholds apply. If only
erroris set,warning=error. An unsetnestinglevel is disabled. -
If the organization sets a baseline (
enforcement.metrics), the project may only lower thresholds, only changeignores_*fromtruetofalseand only raiseseverity.
rules:
metrics:
severity: error
line_length: { warning: 120, error: 160, ignores_urls: true }
function_body_length: { warning: 40, error: 80 }
cyclomatic_complexity: { warning: 10, error: 20 }
nesting: { type_level: { warning: 2 }, function_level: { warning: 3 } }
config validate then shows every effective threshold with its source:
metrics: rules.metrics.severity = error (built-in, locked: false)
metrics: rules.metrics.line_length.warning = 120 (project, locked: false)
metrics: rules.metrics.line_length.error = 160 (project, locked: false)
metrics: rules.metrics.line_length.ignores_urls = true (project, locked: false)
custom_rules
Your own rules. In 0.3.5, all fields except pattern and suggestion must be specified; otherwise config validate reports configuration value has the wrong type.
| Field | Values | Required |
|---|---|---|
id | ^[a-z][a-z0-9-]{2,63}$, not codeguard-…, not a built-in ID | yes |
engine | regex or file (dependency, architecture are not implemented) | yes |
severity | info, warning, error, critical | yes |
scopes | list of file, diff, project, operation | yes |
autofix | only false | yes |
paths | { include: [...], exclude: [...] }, specify both lists | yes |
pattern | regular expression, at most 4096 characters; forbidden with engine: file | with regex |
message | at most 500 characters | yes |
suggestion | at most 1000 characters | no |
custom_rules:
- id: no-debug-print
engine: regex
severity: warning
scopes: [file, diff, project]
autofix: false
paths: { include: ["Sources/**"], exclude: [] }
pattern: "debugPrint\\("
message: "debugPrint gehört nicht in Produktionscode."
suggestion: "Logger verwenden."
- id: no-env-files
engine: file
severity: error
scopes: [diff, project]
autofix: false
paths: { include: ["**/*.env"], exclude: [] }
message: ".env-Dateien nicht einchecken."
Custom rules can never be waived. The matched text doesn't appear in the report.
waivers
A waiver accepts one specific finding as an accepted risk. The finding stays in the report but no longer blocks (disposition: accepted_risk, counted under summary.accepted_risk).
| Field | Meaning |
|---|---|
id | unique ID |
rule | rule ID of the finding (built-in rules and codeguard-metric-* only) |
fingerprint | sha256:<64 hex> from the finding's fingerprint field in the JSON report |
paths | globs the waiver is restricted to (not empty) |
reason | justification (not empty) |
owner | responsible person or group (not empty) |
created_at, expires_at | timestamps, e.g. 2026-10-01T00:00:00Z; expires_at must be after created_at, and the waiver must be currently valid |
Maximum duration: 90 days, 7 days for critical. The organization can lower this further with limits.max_waiver_days. Not waivable are protected-file-change, path-outside-allowed-scope, all run diagnostics (codeguard-… except codeguard-metric-…), custom rules, swift-format, swiftlint.* and rules the organization lists in non_waivable_rules.
This is how you get the fingerprint:
codeguard --format json check-project | jq '.violations[] | {rule, file, fingerprint}'
waivers:
- id: W-001
rule: macos-app-sandbox-missing
fingerprint: "sha256:3f766684cfc82777b3ca10fb481c1154bed31576d0f9a3e45301457d7a741496"
paths: ["App.entitlements"]
reason: "Debug-Build außerhalb des App Store, Ticket APP-123"
owner: "team-mac"
created_at: "2026-10-01T00:00:00Z"
expires_at: "2026-12-01T00:00:00Z"
A critical waiver lasting more than 7 days fails:
configuration validation failed: project:/waivers/0/expires_at: waiver duration exceeds the severity maximum
The fingerprint contains no line number. A waiver therefore still matches when code above it moves. It no longer matches when the affected line itself changes. For platform rules, it is tied to target, file, key and configuration.
execution
The project level may only set analysis_limits here. All other values can only be changed by the organization via locked_values.
execution.analysis_limits (project may set):
| Key | Default | Maximum | Applies to |
|---|---|---|---|
max_plist_bytes | 1 MiB | 64 MiB | plist, .entitlements, .xcprivacy |
max_pbxproj_bytes | 16 MiB | 256 MiB | project.pbxproj |
max_source_bytes | 2 MiB | 64 MiB | Swift/ObjC/C sources, .xcconfig, contents.xcworkspacedata |
max_text_bytes | 2 MiB | 64 MiB | other text files |
max_files | 20 000 | 1 000 000 | number of files in a run (if exceeded, the content check evaluates no file) |
max_include_depth | 16 | 64 | depth of #include in .xcconfig chains |
The minimum is 1 in each case. Specify values in bytes, e.g. max_text_bytes: 4194304.
execution.timeouts (organization only): duration as a number plus s, m, h or d.
| Key | Default | Applies to |
|---|---|---|
preflight | 10s | queries before the build (dump-package, -showsdks, simctl list, -showBuildSettings) as a shared budget |
format_file | 30s | swift-format per file |
format_batch | 2m | swift-format per run |
swiftlint | 5m | SwiftLint runs |
static_analysis | 2m | content rules and platform checks |
compile | 20m | every build in the matrix, including -showdestinations and -list |
tests | 30m | every test run |
simulator_boot | 3m | creating and booting a simulator |
simulator_cleanup | 1m | shutting down and deleting a simulator |
termination_grace | 5s | wait time after terminating a tool |
project_total | 60m | not an overall limit for the run; only used to detect orphaned simulators |
guard_operation, report, check_file_total | 5s, 30s, 5m | no effect in 0.3.5 |
Other values (organization only):
| Key | Default | Effect |
|---|---|---|
max_workers | 4 (1–4) | parallel evaluation of the content rules |
output_limit_mib_per_stream | 10 (1–10) | upper limit per output stream of a tool |
concurrency, reserve_logical_cpus, cache.enabled, cache.location | auto, 2, true, user-cache | no effect in 0.3.5 |
security
Only the organization can change anything here.
| Key | Default | Effect |
|---|---|---|
security.network.allowed_destinations | [] | Allowlist for unauthorized-network-destination. Format scheme://host[:port] or scheme://*.host[:port]. The old format host[:port] is exit 2. Empty: rule inactive. |
security.fail_closed, security.offline, security.redact.enabled | true | always true, can't be changed |
security.build_scripts, security.shell.*, security.network.default_decision, security.approvals.*, security.redact.replacement | see schema | no effect in 0.3.5 |
tools
Paths and version constraints of the tools. By default, only the organization can set them.
| Key | Default | Effect |
|---|---|---|
tools.swift_format.executable / .version_constraint | null | explicit path or version constraint (e.g. >=6.3.0,<6.4.0) |
tools.swiftlint.executable / .version_constraint | null | same as above |
tools.swift.executable / .version_constraint | null | same as above |
tools.xcodebuild.executable / .version_constraint | /usr/bin/xcodebuild / default range >=26.0.0,<28.0.0 | same as above |
tools.resolution | trusted-paths | only value |
An explicit path must be under a trusted directory of the policy. A version_constraint set for swift-format excludes the unversioned Xcode 27 build (main).
diff
| Key | Default | Effect |
|---|---|---|
diff.detect_renames | true | detect renames and copies (git diff -M -C --find-copies-harder) |
diff.include_untracked | true | no effect in 0.3.5. Untracked files are controlled only by check-diff --include-untracked. |
diff.static_findings.fail_on, diff.baseline | [introduced, touched], null | no effect in 0.3.5 |
Keys without effect in 0.3.5
These sections exist in the schema and are validated, but don't affect the run (they only go into the configuration_hash):
autofix.*(there is no autofix;autofix.max_iterationsonly appears asrun.max_iterationsin the report)reports.*(format and destination come from--formatand--output)logging.level,logging.audit.retention_days,logging.audit.include_tool_raw_output; the audit log is always onci.*
The configuration_hash is part of every trust ticket. Any change to the effective configuration, even to a key without effect, invalidates existing tickets. After that, you need to run codeguard trust grant again.
Complete example
version: 1
project:
kind: auto
platforms: [macos, ios]
paths:
include: ["Sources/**", "Tests/**", "Package.swift"]
exclude: [".build/**", "DerivedData/**", "Vendor/**", ".git/**", "Tests/Fixtures/**"]
checks:
platform: { enabled: true, required: true, scopes: [file, diff, project] }
tests: { simulator: true, ipad: true }
rules:
forbidden-fatal-error:
severity: error
sensitive-data-in-log:
sensitive_identifiers: [token, accessToken, refreshToken, password, secret, apiKey, iban]
metrics:
line_length: { warning: 120, error: 160, ignores_urls: true }
function_body_length: { warning: 40, error: 80 }
custom_rules:
- id: no-debug-print
engine: regex
severity: warning
scopes: [file, diff, project]
autofix: false
paths: { include: ["Sources/**"], exclude: [] }
pattern: "debugPrint\\("
message: "debugPrint gehört nicht in Produktionscode."
execution:
analysis_limits:
max_text_bytes: 4194304