Guidance for Claude Code and other AI assistants working in this repository.
CybICS is an open-source ICS security training platform: a simulated gas pressure
control system (OpenPLC, FUXA, OPC-UA, S7comm, an IDS, CTF modules) that runs
virtually in Docker or on a Raspberry Pi Zero 2 W with a custom PCB and an STM32
running Zephyr. Component details live next to the code in doc/README.md,
software/README.md, software/*/README.md, hardware/README.md,
tests/README.md and training/README.md; read those before changing a component.
Everything that lands in git or on GitHub is written in English: commit messages, branch names, PR titles and bodies, issues, review comments, code comments and documentation. Reply to the user in whatever language they write, but never carry that language into a git artefact. If the user hands you a German issue or PR text to file, translate it rather than pasting it verbatim.
Commits follow Conventional Commits: <type>(<scope>): <subject>.
- Types:
feat,fix,docs,test,ci,build,refactor,perf,chore. Dependabot usespipandcias prefixes; leave those alone. - Scopes are the component directories:
stm32,hwio-virtual,hwio-raspberry,openplc,fuxa,opcua,s7com,landing,ids,agent,attack-machine,engineeringws,rpi-image,tests,training,hardware,devcontainer. Omit the scope when a change spans several components. - Subject: imperative, lower-case first letter, no trailing period, at most 72
characters. A breaking change gets
!after the scope and aBREAKING CHANGE:footer. - Body: explain why, not what the diff shows. State what was verified and how (which tests, against which stack, on which architecture). The history since 2026 has many good examples; match that depth.
- Keep the attribution trailers the tooling adds (
Co-Authored-By,Claude-Session).
Branches are feature/<topic>, fix/<topic>, docs/<topic>, ci/<topic>. All
work reaches main through a PR with one approving review. PRs are squash
merged, so the PR title must itself be a valid Conventional Commit subject. A PR
description has three sections: Cause, Fix, Verified. Stacked PRs say
so in the first line and name their base branch.
Never stage .env, .dev.env, .docker.env/, software/build/, anything
under software/rpi-image/deploy/ or pi-gen/work/, historian.sqlite, *.log.
They are gitignored; if one shows up in git status, the checkout is wrong, not
the ignore file.
cybics.sh: user-facing entry point (start,stop,status,logs,update,clean,compose). Drives.devcontainer/virtual/docker-compose.yml;--mode minimal|full|withoutaiselects the compose profiles..devcontainer/:virtual/(the full Docker testbed),stm32/(Zephyr toolchain),raspberry/(arm64 image builds).prepare-env.shgenerates the.envfiles and must run before any manualdocker compose.software/: one directory per service.stm32/holds the reference physical process model;hwio-virtual/mirrors it for Docker;hwio-raspberry/bridges I²C/GPIO on real hardware.landing/is the Flask CTF hub,ids/the rule-based IDS,cybicsagent/the Ollama RAG assistant,rpi-image/the pi-gen SD image (pi-gen/is a submodule, as isOpenPLC/OpenPLC_v3).build.shandinstallRPI.shserve the physical deployment.tests/: pytest suite, mostly against a live stack.training/: one directory per module;training/README.mdhas the learning path and the MITRE ATT&CK for ICS / D3FEND / NIST SP 800-82r3 mapping.hardware/: KiCad 8 PCB, KiBot config, enclosure. Generated docs are committed by CI.
./cybics.sh start # full stack; --mode minimal for core services
pip install -r tests/requirements.txt # nmap must be installed on the host
pytest tests/ --verboseBefore opening a PR, run the full suite against a running stack. That is the acceptance bar for every change, not only for changes to the tested component. Do not open a PR with a suite you did not run.
tests/conftest.pygates the session on real readiness and fails when a service is missing. A stack that is down is a failure, never a reason to skip.- Always run the whole directory. A hand-picked file list is how a new test file once silently never ran in CI.
TEST_SERVER_IPandTEST_<SERVICE>_PORTpoint the suite at a remote or relocated stack.test_ids_rules.pyandtest_plant_model_parity.pyneed no stack and are a quick check, not a substitute.
Firmware (Zephyr v4.3.0, SDK 0.17.4, board nucleo_g070rb):
docker compose -f .devcontainer/stm32/docker-compose.yml run --rm dev scripts/build.shCI builds with warnings as errors; a warning in software/stm32/src/ is a
failed build. The toolchain is x86_64 only, see the comment in software/build.sh.
Claude has no access to the physical hardware. Changes to hwio-raspberry,
STM32 flashing, installRPI.sh or rpi-image can only be build-checked; say so
in the PR's Verified section.
Nine workflows in .github/workflows/ must stay green. Two are unusual:
kibotVerify.yml auto-commits regenerated PCB docs back to the branch, and
rpiImage.yml builds the SD image only on v* tags and attaches it to the release.
- Two plant models.
thread_physicalinsoftware/stm32/src/main.cis the reference;physical_process_threadinsoftware/hwio-virtual/hardwareAbstraction.pymirrors it.tests/test_plant_model_parity.pyparses both with regexes and fails on drift. Change rates or guards in both, and update the parity test if you restructure either function. - Container IPs (
172.18.0.0/24) live in.devcontainer/virtual/docker-compose.ymland again inKNOWN_SERVICESinsoftware/ids/rules.py. - Adding a service touches
.devcontainer/virtual/docker-compose.yml, theIMAGESlist inpushDockerRepos.yml,software/scripts/build-and-push-versioned.sh,display_servicesincybics.sh,KNOWN_SERVICESinsoftware/ids/rules.py,REQUIRED_SERVICESintests/conftest.pyandsoftware/docker-compose.yaml. - Zephyr and SDK versions:
software/stm32/west.yml,ZEPHYR_SDK_VERSIONinbuildTest.yml, thecompilerPathin.devcontainer/stm32/devcontainer.json. - pymodbus is on one version everywhere except
software/landing/, which is still on 3.5.x with no recorded reason. The landing container carries a copy oftraining/and runs the modules from its webshell, so a bump there must be verified from that webshell. Do not bump it in passing. - Python dependencies are pinned exactly and managed by Dependabot. Only
training/requirements.txtis intentionally unpinned.
software/build/, software/stm32/proto/cybics.pb.{c,h} (nanopb, from
cybics.proto), hardware/pcb/docs/ and hardware/pcb/pcb/ (KiBot),
software/FUXA/fuxa-project.json (export from the FUXA UI), software/OpenPLC/openplc.db.
The OPC-UA test key in software/opcua/certificates/trusted/, the credential
list in training/opcua/credentials.txt, the Wi-Fi PSK, the OpenPLC and OPC-UA
demo logins and the UART menu password are deliberate parts of a training
platform. Leave them, do not rotate them, do not report them as findings.
TruffleHog only fails on verified live credentials, which is the actual bar.
The only real secrets are CI secrets. The Docker Hub push token is confined to
pushDockerRepos.yml; every other workflow uses the read-only token, guarded so
fork PRs fall back to anonymous pulls. Keep it that way.
- A module is
training/<name>/README.md, optionally with a solution script and its ownrequirements.txt. Match the existing tone: technique, steps, then how to detect or defend. - Every module has a row in the mapping table in
training/README.md. Check technique IDs against the current MITRE catalogue; wrong IDs have shipped before. - Flags have the form
CybICS(...)and live insoftware/landing/ctf_config.json,software/ids/ids_server.py,software/landing/app.pyand the firmware. A new challenge needs itsctf_config.jsonentry and a place where the flag is actually obtainable. Watch for0versusO. tests/test_training.pyexercises the attack scenarios; add a test when a module has a verifiable effect.
landing and ids run with host networking and NET_ADMIN/NET_RAW; the
landing page also mounts the Docker socket. Changes there affect the host, not
only the container. Use the smallest capability that does the job and mention
the blast radius in the PR.