Skip to content

Using portal profiles in CI

How to use .codecharter/config.yml, the lockfile, and the CLI commands restore/update/verify to run portal-managed profiles in your CI.

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

  1. Open the extension settings (Ctrl+,, search for "CodeCharter").
  2. If the codecharter CLI is not on your PATH, point the codecharter.serverPath setting at the binary. The portal URL can be changed via codecharter.portalBaseUrl (default: https://codecharter.tools).
  3. Open a workspace that contains a .codecharter/config.yml and 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.

Further reading