CLAUDE.md / AGENTS.md
July 16, 2026 · View on GitHub
This file provides guidance to coding agents (Claude Code, Cursor, Codex, etc.) when working in this repository.
The canonical file is AGENTS.md at the repo root. CLAUDE.md is a symlink to it, so agents that look for either name find the same content. Keep edits in AGENTS.md.
Repository overview
This repo holds the pre-built packages consumed by the NVIDIA NodeWright operator (the operator repo is NVIDIA/nodewright, also known as skyhook). A package is a container image that the operator runs on a node to install, configure, tune, or clean up host-level software through a defined lifecycle.
This repo does not contain the operator, the agent, or any Go code. It is shell scripts (could be python, or any executable, but currently is all shell), Dockerfiles, package config, and Python-based tests/tooling.
Rename status (Skyhook to NodeWright): the project is being renamed from Skyhook to NodeWright. New package releases publish at ghcr.io/nvidia/nodewright-packages/...; older package versions may still exist only under the legacy ghcr.io/nvidia/skyhook-packages/... namespace. The CRD group (skyhook.nvidia.com/v1alpha1), the on-host SKYHOOK_DIR/skyhook_dir names, and the CLI still use skyhook on purpose. The GitHub repo itself is now nodewright-packages (with skyhook-packages redirecting). Do not rename remaining skyhook references without checking their specific compatibility contract.
Repository layout
- Each top-level directory that contains a
Dockerfileis a package. Current packages:shellscript,tuning,tuned,kdump,nvidia-setup,nvidia-tuning-gke,nvidia-tuned,copy-fail,bind-mount. scripts/: repo tooling (validate.py,sync_labels.py).tests/integration/: Docker-based pytest suites, one subdir per package that has tests (with hyphens converted to underscores, e.g.nvidia-setuptonvidia_setup; not every package has a subdir)..github/: workflows, thebuild-packagecomposite action, issue/PR templates,labels.yml,CODEOWNERS.docs/: note thatdocs/plansanddocs/superpowersare gitignored;docs/is not a tracked doc tree in this repo. Authoritative prose lives in the root*.mdfiles below.
Package internal structure
<package>/
├── Dockerfile # copies package contents to /skyhook-package in the image
├── config.json # package manifest, validated against the agent schema
├── README.md # per-package usage docs
├── root_dir/ # files copied verbatim to the host root: root_dir/foo -> /foo
└── skyhook_dir/ # lifecycle scripts and static files the scripts use
Adding a new package
A package becomes "real" to CI once its directory has a Dockerfile. Beyond the
package contents themselves, a few repo-wide places track the package set and must
be updated in the same change, or its label automation silently goes stale:
.github/labels.yml: add apackage/<name>label (same color family as the others). Runmake labelsto sync it to GitHub..github/labeler.yml: add apackage/<name>entry mapping<name>/**so PRs touching the package get labeled..github/ISSUE_TEMPLATE/bug_report_form.ymlandfeature_request_form.yml: add<name>to the "Which package does this relate to?" dropdown so reporters can select it..github/workflows/triage.yaml: add<name>to thePACKAGESlist (keep the longest-name-first ordering so substring names liketuneddon't shadownvidia-tuned).
Keep these four in sync with each other; the dropdown value, the label suffix, the
labeler glob directory, and the triage PACKAGES entry are all the same package
directory name.
How packages work (the contract you must not break)
- Everything under
/skyhook-packagein the image lands at${SKYHOOK_DIR}on the host.skyhook_dir/lands at${STEP_ROOT}.root_dir/is laid down at/. config.jsonmust comply with the skyhook agent schemas v1. It declaresschema_version,package_name,package_version(semver), and amodesmap that points each lifecycle stage at a script, arguments, accepted return codes,on_host, andidempotence.- Runtime metadata available to scripts:
STEP_ROOT,SKYHOOK_DIR,${SKYHOOK_DIR}/configmaps(the package's configmaps), and${SKYHOOK_DIR}/node-metadata/(labels.json,annotations.json,packages.json). - Lifecycle stages (see
PACKAGE_LIFECYCLE.mdfor the full contract):apply,config,interrupt,post-interrupt,upgrade,uninstall. Every action stage has a paired-checkmode that validates the action succeeded. Stages must be idempotent.- Normal flow:
uninstall -> apply -> config -> interrupt -> post-interrupt - Upgrade flow:
upgrade -> config -> interrupt -> post-interrupt
- Normal flow:
- Inherited packages build
FROManother package's published image (theFROMline containsskyhook-packagesornodewright-packages). Examples:nvidia-tunedinherits fromtuned,nvidia-tuning-gkefromtuning. An inherited package may omitconfig.jsonif it does not need its own; a standalone package must have one.
Working in this repo: do this before committing
Run from the repo root unless noted. See DEVELOPER.md for the full pre-commit checklist.
- License headers: source files (
*.py,*.sh,*.yaml/*.yml,Dockerfile) carry the full Apache 2.0 block header from.github/license-header.tmpl; Markdown and other docs do not need one. Runmake license-fmtto add/refresh headers andmake license-checkto verify (theLicense Headersworkflow runs the check on every PR and also fails on a duplicated header). Both wrapgoogle/addlicenseviago run, so they need a local Go toolchain; addlicense is idempotent and never duplicates a header (it replaced the oldformat_license.py, which did, see #74). - Validate config:
make validate-standalone PACKAGE=<name>for standalone packages, ormake validate-inherited PACKAGE=<name>for inherited ones (this builds the image first, then validates the assembled/skyhook-package). Validation runs in theghcr.io/nvidia/skyhook/agent:latestcontainer and needs Docker or Podman. - Test:
make testruns the whole Docker-based suite in parallel;make test-package PACKAGE=<name>runs one package's tests. Tests need a working Docker daemon and create avenv/. - Changelog (per package):
<package>/CHANGELOG.mdis generated from Conventional Commit history bygit-cliff(seecliff.toml); do not hand-edit it in a PR. Usemake changelog-preview PACKAGE=<name>to preview the unreleased notes your commits will produce. The file is (re)written bymake changelog PACKAGE=<name>at release time (scoped by--include-path <package>/**and--tag-pattern <package>/.*), so a PR that touchesCHANGELOG.mdwill conflict with that regeneration and should be reverted. Put any hand-authored upgrade/behavior notes in the package'sRELEASE_NOTES.mdinstead. - Labels:
make labelssyncs.github/labels.ymlto GitHub (needsghwith write access).
Commit and version conventions
- Conventional Commits, with the package name as scope:
feat(shellscript): ...,fix(tuning): .... This applies to both commit messages and PR titles: a squash-merge uses the PR title as the commit subject. Usegeneral(orgeneral/ci) as the scope for repo-wide or CI changes, and use exactly one type per message; combined types likedocs+ciare not valid Conventional Commits. - CC is enforced in CI.
.github/workflows/commit-linting.yamlruns commitlint (@commitlint/config-conventional, configured incommitlint.config.js) against the PR title and every commit in the PR, so an invalid type, a non-CC subject, or a combined type likedocs+cifails the check. The subject-case rule is relaxed so capitalized subjects pass, and scope is not restricted to an enum. Lint locally withecho "type(scope): subject" | npx @commitlint/cli@21 --config commitlint.config.js(npx fetches commitlint if it is not installed); CI is the source of truth. - Sign-off is required (DCO). Use
git commit -s. - Semantic versioning per package. Bump
package_versionin the package'sconfig.json, then tag as<package>/<version>(e.g.tuned/1.3.0). Each package versions independently.
Writing style
For prose you author in this repo (docs, READMEs, PR descriptions, commit messages, review comments): do not use em-dashes (—). Use a colon, semicolon, comma, or full stop instead, and recast the sentence when no punctuation swap reads cleanly. Leave intact things that are not authored prose: en-dashes in numeric ranges (24–27), markdown horizontal rules (---), and any em-dash already inside quoted text or code.
Keep markdown well-formed: a single # H1 per file with headings nested in order (no skipped levels), fenced code blocks that carry a language hint (```bash, ```yaml), blank lines around headings, lists, and code fences, consistent list markers, tables with aligned pipes and a header separator row, and links/relative paths that actually resolve. When you edit a doc, preview the rendered markdown rather than trusting the raw text.
Shell scripting conventions
Almost all package logic is bash. The existing scripts are not yet consistent (shebangs and set flags vary), so when you add or touch a script, move it toward this standard rather than matching the nearest neighbour:
- Shebang:
#!/usr/bin/env bash. - Strict mode: start scripts with
set -euo pipefailso they fail fast on errors, unset variables, and broken pipes. Lifecycle-checkscripts are the deliberate exception where a non-zero exit is the signal, not a crash; handle expected failures explicitly there. - Quote every expansion:
"${var}","$@","$(cmd)". Unquoted expansions (shellcheckSC2086) are by far the most common issue in this repo and the main thing to avoid adding. - Prefer
[[ ... ]]over[ ... ],$(...)over backticks, and declare-then-assign for command substitution (local x; x="$(cmd)") so a failing command is not masked (SC2155). - Idempotence: lifecycle scripts must be safe to re-run (see
PACKAGE_LIFECYCLE.md); either rely on the agent's idempotence tracking or write the script so repeats are no-ops. - Lint locally before pushing:
shellcheck path/to/script.sh. CI runs shellcheck across all tracked*.shfiles whenever a.shfile,.shellcheckrc, or the workflow changes (advisory today; it reports findings without blocking, and will become a required check as the count reaches zero).SC1091(can't follow runtime-sourced files like${SKYHOOK_DIR}/...) is suppressed via.shellcheckrc.
CI and release mechanics
- On PR (
pr_build.yaml): detects which package directories changed, then for each changed package runs validate, test, and a dev build tagged<version>-dev.sha<short>. Changed-package detection keys on a directory having aDockerfile;scripts/and hidden dirs are skipped. - On tag push
<package>/<version>: two independent workflows run.build_container.yamlbuilds and pushes the multi-arch image (linux/amd64,linux/arm64) with a build-provenance attestation;release.ymlcreates the GitHub Release withgit-cliffnotes scoped to that package. The image build (here and inpr_build.yaml) goes through the.github/actions/build-packagecomposite action. (build_package.ymlis a reusableworkflow_callworkflow that exists but is not currently wired up.) - Published image path for new releases:
ghcr.io/nvidia/nodewright-packages/<package>:<version>. Verify legacy versions before changing an existing consumer because some older tags remain only underghcr.io/nvidia/skyhook-packages/. - An optional
<package>/preprocess.shis run before the image build and can emitBUILD_ARGS.
Code review (CodeRabbit)
.coderabbit.yaml configures CodeRabbit's automated PR review. Keep it in step with the repo so reviews stay useful, and update it in the same change that introduces the drift:
- Guidance docs: add new repo-guidance files to
knowledge_base.code_guidelines.filePatterns(currentlyAGENTS.md,CONTRIBUTING.md,README.md,DEVELOPER.md,PACKAGE_LIFECYCLE.md) so reviews enforce our conventions. - Generated or vendored content: add new such paths to
reviews.path_filters(currently the git-cliff-generatedCHANGELOG.mdandRELEASE_NOTES.md) so CodeRabbit does not review non-authored files. - Tools: keep
reviews.toolsaligned with the linters we actually use.shellcheckis on and shares rules with CI via.shellcheckrc; revisityamllint(currently off) if malformed YAML starts slipping through.
Keeping software current
Prefer the latest stable versions of the software this repo depends on, and keep
them moving forward rather than letting them drift. This covers base images in
Dockerfiles, the tools the lifecycle scripts invoke, the Python deps under
tests/, and the uses: actions in workflows. Staying current is how we pick up
upstream security and bug fixes.
Balance "latest" against reproducibility and supply-chain safety:
- Pin GitHub Actions to a commit SHA with the human-readable version in a
trailing comment (e.g.
actions/labeler@f27b...e213 # v6.1.0). To update, bump the SHA and the comment together; do not float a@v6tag. - When you touch a file that references a pinned action, a base image tag, or a tool version, check whether a newer stable release exists and update it if the bump is safe. Treat a stale pin you are already editing as worth refreshing.
Gotchas
- A new package is "real" once it has a
Dockerfile; that is what CI keys on. - Do not add a
config.jsonto an inherited package unless it genuinely needs its own; CI treats a missingconfig.jsonas valid only when theDockerfileinherits from askyhook-packages/nodewright-packagesimage. - Test directory names use underscores while package directories use hyphens (
nvidia-setuptotests/integration/nvidia_setup). docs/plansanddocs/superpowersare gitignored working areas, not shipped docs.
Key references
README.md: repository and per-package overviewPACKAGE_LIFECYCLE.md: the lifecycle-stage contractDEVELOPER.md: validation and pre-commit stepsCONTRIBUTING.md: DCO sign-off and commit style- NodeWright operator (
NVIDIA/skyhook): the operator that runs these packages - Agent schemas v1: the
config.jsoncontract