Contributing to IronClaw

August 1, 2026 · View on GitHub

By contributing to this repository, you agree to grant the project maintainers a permanent, non-exclusive, worldwide, royalty-free license to use, modify, and commercially dual-license your contributions. This is formalized in the Contributor License Agreement, which the CLA Assistant bot asks you to sign on your first pull request. Sign it in one click at https://cla-assistant.io/IronSecCo/ironclaw. Posting a sign comment on the pull request does not work here, see Signing the CLA.

Thanks for your interest in IronClaw! Contributions of every kind are welcome — bug reports, fixes, new channel adapters, docs, and tests.

New here? The documentation site is the best starting point — architecture, threat model, quickstart, channels, and skills in one place.

Quickstart — your first PR in 5 minutes

From a clean checkout (Go 1.23+, with CGO_ENABLED=1 for the SQLCipher binding):

# 1. Fork on GitHub, then clone your fork
git clone https://github.com/<you>/ironclaw && cd ironclaw

# 2. Build and run the checks (this is exactly what CI runs)
export CGO_ENABLED=1
make build vet test      # or: go build ./... && go vet ./... && go test ./...

# 3. Branch, make your change, and verify formatting
git checkout -b my-change
gofmt -l .               # must print nothing

# 4. Commit and push to your fork, then open a PR against `main`
git commit -am "docs: fix a typo"
git push -u origin my-change

Then open the pull request, fill in the template, and link the issue it closes. A maintainer reviews and merges.

Looking for something to work on? Grab a good first issue (comment to claim it first), or ask in Discussions. The rest of this guide covers the ground rules, the frozen contract, and the code layout in detail.

Signing the CLA

One click, and it is the only setup step there is:

https://cla-assistant.io/IronSecCo/ironclaw

The signature is recorded against your GitHub account, so you sign once and every later pull request is covered. The CLA Assistant bot posts that same link on your first PR.

Do not try to sign by comment. Other projects accept a I have read the CLA Document and I hereby sign the CLA comment on the pull request. This one does not: the bot we run only reads pull request events, so a sign comment is silently ignored and license/cla stays red no matter how many times you post it. If you have signed and the check is still red, use the recheck link in the bot's comment rather than replying to it.

Good places to start

Two subsystems are deliberately self-contained — you can make a real, mergeable change in either without loading the whole architecture in your head:

  • ironctl scan (internal/host/scan/, cmd/ironctl/scan.go) — grades a container's containment posture 0-100 across seven dimensions. Pure, table-testable scoring logic and a small CLI surface. Good for test coverage, new flags, and output polish. See docs/scan.md.
  • The isolation scores dataset (examples/isolation-survey/) — the data behind the public container isolation scores directory. Adding images to images.txt is a data-only change (no Go, no build); the weekly CI survey grades and publishes them for you.

Filter the open issues for these areas with the area:cli, area:sandbox, and category:test labels.

Ground rules

  • Open an issue first for anything non-trivial, so we can agree on the approach before you invest time.
  • Keep changes small, focused, and reversible — one concern per pull request.
  • No secrets in code, tests, fixtures, or logs.
  • Be excellent to each other — see the Code of Conduct.

Contributor License Agreement (CLA)

Before your first contribution can be merged, you'll sign our Contributor License Agreement. It confirms you have the right to contribute your work and lets IronSecCo offer IronClaw under both the open-source AGPLv3 and a commercial license (the project's dual-license model).

There's nothing to do up front: when you open your first pull request, the CLA Assistant bot comments with a link, and you sign in one click with your GitHub account. It remembers your signature for future PRs.

Where we have accepted a contribution without a captured signature, and on what evidence, is on the public record in CLA exceptions. Its companion page, Merge exceptions, records every commit that reached main without the approving review the branch ruleset requires. Both exist because an exception that is not written down is indistinguishable from a process failure.

Development setup

IronClaw is Go 1.23+ and requires CGO_ENABLED=1 (the SQLCipher binding behind the encrypted queues). Before opening a PR, make sure the standard checks pass:

export CGO_ENABLED=1
gofmt -l .      # must print nothing
go vet ./...
go build ./...
go test ./...

make build vet test runs the same checks. See docs/building.md for the full build notes.

Examples smoke gate (credential-free)

Changes under examples/** (or the demo control-plane / sandbox they exercise) trigger the Example smoke workflow. It runs hello-ironclaw, the red-team-escape proof, and a credential-free smoke matrix over every examples/*/run-mock.sh recipe against the offline mock provider — no model key, no channel tokens.

Each run-mock.sh must fail-closed on an empty assistant reply. The chat /messages route is drain-on-read and the reply text lives in .messages[].content (NOT .text, which is the /chat/send request field); reading the wrong key silently drains the reply to empty. IRO-279 shipped exactly that regression — the scripts still exited 0 while returning nothing. So when you add or edit a mock recipe, keep the non-empty assertion (return 1 / exit 1 on an empty poll) so a wrong-field or broken round-trip turns the check red instead of passing silently. The sandbox image and demo control-plane are built once and shared across the matrix, so keep each recipe's own runtime tight. The job is additive and non-gating (not a required check).

You can run the whole matrix locally:

bash container/build.sh                                # build ironclaw-sandbox:latest once
docker compose -f docker-compose.demo.yml up --build -d
for s in examples/*/run-mock.sh; do bash "$s" || echo "BROKE: $s"; done
docker compose -f docker-compose.demo.yml down

The frozen contract

internal/contract/** is the single seam shared by the control-plane (host) and the sandbox — the only package both sides import. It is frozen: a drift here is a silent decrypt failure or routing mismatch at runtime, not a build error. Every file in it carries the banner // FROZEN CONTRACT — do not edit without a joint RFC (see docs/contract.md).

Changing it requires:

  1. A joint RFC entry appended to docs/contract.md.
  2. Approval from the code owners (see CODEOWNERS).

Code layout

AreaPath
Control-plane / hostinternal/host/**, cmd/controlplane, cmd/ironctl, api/, deploy/
Sandbox runtimeinternal/sandbox/**, cmd/sandbox
Shared frozen seaminternal/contract/** (see above)
Behavioral parity suitetest/parity/** (shared — add specs, don't rewrite others')

Documentation

The docs site (MkDocs Material, under docs/) has no shared nav: block in mkdocs.yml. The navigation is assembled at build time from per-directory .nav.yml fragment files, so two docs PRs almost never touch the same nav line and no longer conflict on merge.

To add a page:

  1. Drop the .md file in the right directory with a top-level # H1 (its title).
  2. For the churn-heavy sections — providers/, tutorials/, integrations/ — that is all you need: the directory's .nav.yml ends with a "*.md" glob that auto-includes any new page (titled from its H1), appended after the explicitly listed ones. Add an explicit - My Label: my-page.md line to that directory's .nav.yml only if you want a custom label or a specific position.
  3. For a page in another section, add one line to the matching section in docs/.nav.yml (the root fragment).

mkdocs build --strict must stay green (pip install -r docs/requirements.txt then mkdocs build --strict). The assembler fails the build loudly if a fragment points at a missing page, so a typo can never silently drop a page. See the top of docs/hooks.py for the fragment format.

Adding a hardening guide: which indexes are exhaustive

Hardening guides (docs/blog/harden-*.md) are listed on four surfaces, and only two of them are meant to list every guide. The split is deliberate, so knowing which is which saves you a round of review:

SurfaceExhaustive?Enforced by
docs/blog/.nav.yml — the blog navYes, every guidescripts/check-guide-index.py in CI
docs/blog/hardening-guides.md — the hubYes, every guidescripts/check-guide-index.py in CI
docs/blog/index.md — the blog landing pageNo, curatedmaintainer, case by case
README.mdNo, curatedmaintainer, case by case

Adding a guide requires the first two and nothing else. Wire your new guide into the nav and the hub, and CI is satisfied — check-guide-index.py fails the build if either is missing, and that is the whole contract.

Do not add your guide to docs/blog/index.md or README.md, and do not treat its absence there as a bug to fix. Neither page claims to list every guide, and most guides are deliberately absent from both — both link the hub, which is the exhaustive one. They are curated for different reasons:

  • docs/blog/index.md gives every entry a hand-written summary quoting that guide's real before/after scores ("scores 48 of 100 (D) … the honest hardened ceiling is 89 of 100 (B)"). Making it exhaustive would mean inventing a summary and a score line per guide, and a rule that pressures an author into fabricating numbers is worse than no rule.
  • README.md carries a short, representative selection of guides as a link list. It is the project's front door, not a catalog, and it stays short on purpose.

Whether a particular guide earns a spot on either is an editorial call a maintainer makes case by case, so please leave it to review rather than backfilling it in a PR.

The same reasoning, from the enforcing side, is in the module docstring of scripts/check-guide-index.py.

Pull requests

  • Branch from main, make your change, and open a PR against main.
  • Fill in the PR template, link the issue it closes, and make sure CI is green.
  • A maintainer reviews and merges. We keep main releasable at all times.

Good first contributions

The fastest way in is a good first issue — these are small, self-contained, and mentored. Comment on one to claim it before you start.

Channel adapters (internal/host/channels/) are also small, uniform, and dependency-free — a great first PR. See Writing a channel adapter for the interface and house pattern, and docs/channels.md for how each existing adapter is configured.

Have a question instead of a change? Open a thread in GitHub Discussions.