Prerequisite: this page assumes you have already created profiles and rules in the portal. If you are new to portal-managed rules, start at Manage profiles.
Profiles you maintain in the portal reach your CI via two files in your
repository, both in the .codecharter/ directory: config.yml (what the project
references) and codecharter.lock.json (frozen versions and hashes). The CLI
syncs, checks, and verifies these files.
Concept: .codecharter/config.yml and .codecharter/codecharter.lock.json
.codecharter/config.yml declares which profiles your project uses. It lives in
your repository's .codecharter/ directory and is found by walking up from the
file being analysed. Each profile entry is a string of the form
[org/][email protected] and pins an exact version; platform
profiles use the codecharter/ prefix:
# .codecharter/config.yml
version: 1
profiles:
- [email protected]
- [email protected]
- codecharter/[email protected]
The same file also supports an optional overrides key to adjust a rule's
severity (error, warning, info), keyed directly by the rule slug. To
switch a rule off entirely, list it under ignore instead. Rule slugs are
unique across profiles, so both sections address rules directly:
# .codecharter/config.yml
version: 1
overrides:
no-async-void:
severity: warning
ignore:
- rule: long-method
.codecharter/codecharter.lock.json is generated by the CLI and committed to
source control. It records the engine version requirement, the portal it was resolved against,
and freezes each profile to a specific version, a SHA-256 hash, and a download
URL:
{
"lockfileVersion": 1,
"engineMinVersion": "3.2.0",
"generatedAt": "2026-06-01T12:00:00Z",
"portalBaseUrl": "https://codecharter.tools",
"profiles": [
{
"slug": "acme-base",
"version": "1.4.0",
"source": "org",
"contentHash": "sha256:e3b0c44298fc1c149afb...",
"bundleUrl": "https://codecharter.tools/...",
"eTag": "\"abc123\"",
"rules": [
{ "slug": "no-async-void", "version": "2.1.0", "contentHash": "sha256:abc123..." }
]
}
]
}
restore downloads bundles from the bundleUrl recorded in the lockfile, so
always let codecharter update generate this file instead of writing it by hand.
Authentication: API key and license
Commands that talk to the portal directly (update, push, and
verify --against-portal) take the API key via their required --api-key
flag. restore does not need an API key: it authenticates bundle downloads
with your CodeCharter license.
The CODECHARTER_API_KEY environment variable serves a different purpose: every
CLI command requires a valid license (exit code 6 when none is found). When
CODECHARTER_API_KEY is set, the CLI automatically fetches a short-lived
license from the portal (GET /api/v1/cli/license) whenever the cached one is
missing or about to expire. That is why you set the secret as an environment
variable on CI steps even when the command itself takes no API key.
CLI commands
restore
codecharter restore [--lockfile <path>] [--cache-dir <path>] [--quiet]
Downloads all profiles and rules listed in the lockfile from the portal and
places them under .codecharter/cache/ next to the lockfile. No download occurs
when the hash already matches (idempotent). This is typically the first step
in CI. Exit codes: 0 success (a missing lockfile is a no-op), 1 bundle
hash drift after download, 2 usage or network error.
update
codecharter update [profile] --portal-url <url> --api-key <key> [--all] [--config <path>] [--lockfile <path>]
Re-resolves the exact profile references declared in .codecharter/config.yml
against the portal and rewrites .codecharter/codecharter.lock.json with fresh hashes and download
URLs. --portal-url defaults to the hosted portal (pass it only for a self-hosted
instance), and authentication falls back to your installed codecharter.license when
--api-key is omitted. Without the [profile]
argument all profiles are updated; with a profile slug only that profile is
re-resolved. You commit the updated lockfile manually and open a PR. In CI you
typically run only restore, not update. Exit codes: 0 success, 1
resolver or portal error, 2 usage error.
verify
codecharter verify [--lockfile <path>] [--cache-dir <path>] [--against-portal] [--portal-url <url>] [--api-key <key>] [--quiet]
By default verify works offline: it hashes the locally cached bundle files
and compares them against codecharter.lock.json. On any mismatch or missing
bundle it fails with exit code 1. With --against-portal it additionally asks
the portal to check the lockfile entries against the stored versions; in this
mode --portal-url defaults to the hosted portal and authentication falls back to
your installed license when --api-key is omitted, and the command fails fast if
the portal is unreachable. Exit codes: 0 clean, 1
drift, 2 usage error (for example, lockfile missing).
Note the cache directory defaults: restore writes to .codecharter/cache
next to the lockfile, while verify defaults to ~/.codecharter/cache. In CI,
pass --cache-dir .codecharter/cache to verify so it checks the cache that
restore actually wrote.
analyze enforces the lockfile
codecharter analyze checks the lockfile itself when .codecharter/config.yml
declares profiles: it exits 2 when the lockfile is missing, runs a quiet implicit
restore when bundles are absent from the cache, exits 3 when that restore
fails because the portal is unreachable, and exits 4 on hash drift. CI
pipelines can gate on these exit codes directly.
GitHub Actions workflow (complete)
The CLI is not preinstalled on GitHub runners. Download it from the portal
(GET /api/v1/cli/{platform}/{version}, authenticated with your API key as a
bearer token; platforms: win-x64, linux-x64, osx-x64, osx-arm64;
version selectors: latest, v1, v1.4, v1.4.2).
.github/workflows/codecharter.yml:
name: CodeCharter
on:
pull_request:
push:
branches: [main]
jobs:
analyze:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4
with:
dotnet-version: '9.0.x'
- name: Install CodeCharter CLI
run: |
curl -fsSL -H "Authorization: Bearer $CODECHARTER_API_KEY" \
-o codecharter-cli.tar.gz \
"https://codecharter.tools/api/v1/cli/linux-x64/latest"
mkdir -p "$HOME/codecharter-cli"
tar -xzf codecharter-cli.tar.gz -C "$HOME/codecharter-cli"
echo "$HOME/codecharter-cli" >> "$GITHUB_PATH"
env:
CODECHARTER_API_KEY: ${{ secrets.CODECHARTER_API_KEY }}
- name: Cache CodeCharter bundles
uses: actions/cache@v4
with:
path: .codecharter/cache
key: codecharter-${{ hashFiles('.codecharter/codecharter.lock.json') }}
restore-keys: codecharter-
- name: Restore portal profiles
run: codecharter restore
env:
CODECHARTER_API_KEY: ${{ secrets.CODECHARTER_API_KEY }}
- name: Verify lockfile integrity
run: codecharter verify --cache-dir .codecharter/cache
env:
CODECHARTER_API_KEY: ${{ secrets.CODECHARTER_API_KEY }}
- uses: bochmann-software/codeguard@v1
with:
solution: Acme.Web.sln
api-key: ${{ secrets.CODECHARTER_API_KEY }}
restore and verify run before the actual analyze step: restore ensures
all bundles are available locally, verify ensures the cache matches the
lockfile. The CODECHARTER_API_KEY environment variable on these steps lets the
CLI mint its short-lived license automatically.
Lockfile cache strategy
The cache key codecharter-${{ hashFiles('.codecharter/codecharter.lock.json') }} means: every
time you update the lockfile, there is a cache miss and a fresh download takes
place. With the same lock version restore completes in seconds.
Add the cache path .codecharter/cache/ to .gitignore:
# .gitignore
.codecharter/cache/
What should be committed to the repository is the .codecharter directory:
.codecharter/config.yml— profile references.codecharter/codecharter.lock.json— frozen versions and hashes
Common errors
Lockfile missing
When .codecharter/config.yml declares profiles but codecharter.lock.json is
missing, codecharter analyze exits with code 2, and codecharter verify prints:
error: lockfile not found at '<path>'.
and exits with code 2. codecharter restore treats a missing lockfile as a
no-op (note: no lockfile found, nothing to restore., exit 0). The lockfile
was not committed: run codecharter update --portal-url <url> --api-key <key>
locally and add .codecharter/codecharter.lock.json to your commit.
Hash drift
codecharter verify reports drift like this and exits with code 1:
DRIFT: 1 entry/entries do not match the lockfile.
[hash-mismatch] [email protected]
expected: sha256:e3b0c44...
actual: sha256:f7a9d12...
codecharter restore fails with
error: bundle hash drift for profile '[email protected]'. Run 'codecharter update' to refresh the lockfile.
(exit 1), and codecharter analyze exits with code 4. The profile in the
portal was changed without updating the lockfile, or the lockfile was tampered
with. Run codecharter update --portal-url <url> --api-key <key> locally to
refresh the lockfile and merge it in a PR.
Portal unreachable
codecharter restore reports failed downloads as
error: failed to download bundle for '<slug>': <message>, and
codecharter verify --against-portal prints
error: portal request failed: <message>. When the cache is incomplete and
the portal cannot be reached, codecharter analyze exits with code 3.
Check that the runner has outbound internet access to
codecharter.tools and that a valid license is available
(restore authenticates with the license, not the API key). Self-hosted
runners in isolated networks need an outbound proxy or a firewall allowance.
Profile version not found
When a version recorded in the lockfile no longer exists in the portal (it was
deleted or belongs to a different org scope), codecharter update fails with a
resolver error (exit 1), and codecharter verify --against-portal reports the
entry as portal-side drift. Running
codecharter update --portal-url <url> --api-key <key> locally resolves this if
a newer version exists.
Rate limit exceeded (429)
The portal rate-limits API requests per identity (each API key has its own
budget): by default 60 GET requests and 10 write requests per minute. Above
the limit, requests fail with 429 Too Many Requests; the response carries
a Retry-After header with the seconds until the budget resets and a JSON
body with the same value in a retryAfter field.
A single CI run stays far below these limits; the usual cause is many
parallel jobs sharing one key. CLI release archive downloads are not
rate-limited, but resolution and verification calls are. Fixes: cache
.codecharter/cache/ keyed on the lockfile (as in the workflow above) so
repeat runs make no portal calls, wait the number of seconds given in
Retry-After before retrying, or give independent pipelines their own API
keys so they do not share a budget.
VS Code
The VS Code extension integrates directly with the offline-first workflow. You do not need to run restore or verify manually.
Setup
- Open the extension settings (
Ctrl+,, search for "CodeCharter"). - If the
codecharterCLI is not on your PATH, point thecodecharter.serverPathsetting at the binary. The portal URL can be changed viacodecharter.portalBaseUrl(default:https://codecharter.tools). - Open a workspace that contains a
.codecharter/config.ymland a.codecharter/codecharter.lock.json.
Automatic restore
When the workspace contains a .codecharter/config.yml that declares profiles, the extension runs codecharter restore once on activation and shows the active rule set in the status bar, exactly as codecharter restore does in CI.
Drift warning
If the lockfile has changed since the last restore, the extension shows a warning notification with a Run restore action. Clicking it re-runs codecharter restore so the local cache matches the lockfile again.
Offline mode
Without an internet connection the extension continues working with the last cached bundles, as long as .codecharter/cache/ is present and your license is still valid.
Integration with codecharter push
codecharter push uploads a directory of local .cgr rule files to the portal and
opens a new draft there, ready to review and publish. A minimal invocation requires
--profile and --version; --portal-url defaults to the hosted portal and
authentication uses your installed license, or an --api-key that carries the
write:rules scope:
codecharter push ./rules --profile my-team-rules --version 1.0.0 \
--portal-url https://codecharter.tools \
--api-key $CODECHARTER_API_KEY
Pass --dry-run to validate the upload without actually writing to the portal.
The VS Code command CodeCharter: Push to portal is not equivalent to this:
it belongs to the portal handoff flow and sends the rule draft you are
currently editing back to the portal; it does not invoke the CLI push
command.