|
| 1 | +<%- |
| 2 | + # OS detection from the module's metadata.json (exposed by PDK as |
| 3 | + # @configs['module_metadata']). Falls back safely when metadata is absent |
| 4 | + # (e.g. `pdk new module` before the interview) -- default to Linux content. |
| 5 | + md = (@configs['module_metadata'] rescue {}) || {} |
| 6 | + os_support = (md['operatingsystem_support'] rescue []) || [] |
| 7 | + os_names = os_support.map { |o| ((o['operatingsystem'] || '').to_s.downcase rescue '') } |
| 8 | + linux_names = %w[redhat centos oraclelinux scientific sles suse opensuse debian ubuntu rocky almalinux amazon fedora virtuozzolinux gentoo archlinux] |
| 9 | + has_windows = os_names.include?('windows') |
| 10 | + # Empty/unknown metadata (e.g. a fresh `pdk new module`) defaults to the Linux section. Modules that |
| 11 | + # support only non-Linux, non-Windows platforms (AIX/Solaris/Darwin) render neither OS section and |
| 12 | + # rely on the OS-agnostic guidance above -- rather than being mislabelled as Linux. |
| 13 | + has_linux = os_names.empty? || os_names.any? { |n| linux_names.include?(n) } |
| 14 | +-%> |
| 15 | +# CLAUDE.md |
| 16 | + |
| 17 | +This file provides guidance to Claude Code (claude.ai/code) and human contributors when |
| 18 | +working with code in this repository. |
| 19 | + |
| 20 | +> **Starting point, not a straitjacket.** This file ships from |
| 21 | +> [puppetlabs/pdk-templates](https://github.com/puppetlabs/pdk-templates) as a baseline of conventions |
| 22 | +> common to CAT-supported modules. **Tailor it to this repo** -- add the specifics Claude actually |
| 23 | +> needs: what the manifests/types/providers/functions here do, module-specific gotchas, and anything |
| 24 | +> learned while working in the codebase. A generic framework alone is not enough to guide good changes. |
| 25 | +> |
| 26 | +> PDK adds this file when a module is first created (`pdk new module`, or `pdk convert` for a module |
| 27 | +> that doesn't have one yet) and then leaves it alone -- it is **not** re-synced or overwritten on |
| 28 | +> `pdk update`. It is yours to edit freely; keep it current as the module grows. |
| 29 | + |
| 30 | +For generic Puppet/PDK workflow that *is* derivable from the files themselves (the full PDK command |
| 31 | +reference, CI matrix mechanics, README badges), see the PDK docs and the README. The notes below are |
| 32 | +the conventions Claude should treat as binding before touching code, plus the things that are not |
| 33 | +obvious from a single file. |
| 34 | + |
| 35 | +--- |
| 36 | + |
| 37 | +## Project layout |
| 38 | + |
| 39 | +A CAT-supported Puppet module uses some subset of this structure -- read what actually exists before |
| 40 | +assuming. Modules come in a few shapes: **manifest modules** (classes/defined types), **type/provider |
| 41 | +modules** (custom resources in `lib/puppet/`), and **task modules** (Bolt tasks, often no manifests). |
| 42 | + |
| 43 | +``` |
| 44 | +manifests/ # Puppet DSL: classes and defined types (.pp) |
| 45 | +lib/puppet/type/ # Custom resource type definitions (Ruby) |
| 46 | +lib/puppet/provider/<type>/ # Providers implementing each type per OS/tool |
| 47 | +lib/puppet/functions/ # Modern Puppet 4.x function API (Ruby) |
| 48 | +lib/facter/ # Custom facts |
| 49 | +lib/puppet_x/ # Shared helper libraries (parsing, utilities) |
| 50 | +functions/ # Puppet-language functions (.pp) |
| 51 | +tasks/ # Bolt tasks (<name>.{rb,sh,ps1} + <name>.json schema) |
| 52 | +plans/ # Bolt plans (.pp) |
| 53 | +files/ # Static files / shell helpers shipped with the module |
| 54 | +data/ + hiera.yaml # Module Hiera data (per-OS defaults) |
| 55 | +templates/ # ERB/EPP templates |
| 56 | +spec/ # Tests (see Testing below) |
| 57 | +metadata.json # Module metadata, dependencies, supported OS matrix |
| 58 | +REFERENCE.md # Generated by `puppet strings` -- do not hand-edit |
| 59 | +``` |
| 60 | + |
| 61 | +- Manifests orchestrate; for type/provider modules the real work lives in `lib/puppet/`. |
| 62 | +- Prefer per-OS defaults in `data/` (Hiera, keyed by `os.family`/`os.name`) over hard-coding in `params.pp`. |
| 63 | +- Regenerate reference docs: `bundle exec puppet strings generate --format markdown --out REFERENCE.md`. |
| 64 | + |
| 65 | +--- |
| 66 | + |
| 67 | +## Common commands |
| 68 | + |
| 69 | +```bash |
| 70 | +bundle install # Install dependencies |
| 71 | +bundle exec rake spec_prep # Install fixture modules from .fixtures.yml (before specs) |
| 72 | +bundle exec rake spec # Run all unit tests |
| 73 | +bundle exec rake parallel_spec # Run unit tests in parallel (faster on large suites) |
| 74 | +bundle exec rake lint # puppet-lint |
| 75 | +bundle exec rake lint_fix # Auto-fix lint offences |
| 76 | +bundle exec rake rubocop # Ruby style checks |
| 77 | +bundle exec rake validate # Syntax-check Ruby, Puppet manifests, and metadata |
| 78 | +bundle exec rake syntax # Puppet manifest + Hiera syntax check |
| 79 | +bundle exec rake metadata_lint # Validate metadata.json |
| 80 | +bundle exec rake release_checks # Full pre-release gate (validate + lint + spec) |
| 81 | + |
| 82 | +# Run a single spec file / example |
| 83 | +bundle exec rspec spec/unit/puppet/provider/<type>/<provider>_spec.rb |
| 84 | +bundle exec rspec spec/unit/puppet/type/<type>_spec.rb:42 # by line number |
| 85 | +bundle exec rspec spec/.../foo_spec.rb -e "creates the resource" # by description |
| 86 | +``` |
| 87 | + |
| 88 | +Acceptance tests (Litmus; require Docker or provisioned targets): |
| 89 | + |
| 90 | +```bash |
| 91 | +bundle exec rake litmus:provision_list[default] |
| 92 | +bundle exec rake litmus:install_module |
| 93 | +bundle exec rake litmus:acceptance:parallel |
| 94 | +``` |
| 95 | + |
| 96 | +--- |
| 97 | + |
| 98 | +## Testing conventions |
| 99 | + |
| 100 | +### rspec-puppet (unit, for manifests) |
| 101 | + |
| 102 | +- Every class/defined type should have `it { is_expected.to compile.with_all_deps }` -- a clean |
| 103 | + compile across the supported OS matrix is the baseline gate. |
| 104 | +- Drive OS variants with `on_supported_os` (reads `metadata.json`) rather than hand-listing facts. |
| 105 | +- Facts come largely from **facterdb** (via `rspec-puppet-facts` / `on_supported_os`) -- you rarely |
| 106 | + need to hand-build fact hashes; override only the specific facts a test needs on top of the OS set. |
| 107 | +- Stub facts via the `:facts` hash or `spec/default_facts.yml`; do not rely on the host's real facts. |
| 108 | +- Provide Hiera test data through `spec/fixtures/hiera/` and a test `hiera.yaml` when behaviour is |
| 109 | + data-driven. Use `let(:params)` / `let(:pre_condition)` for class params and dependencies. |
| 110 | +- Fixture modules are declared in `.fixtures.yml` and installed by `rake spec_prep`. |
| 111 | + |
| 112 | +### Type/provider unit specs |
| 113 | + |
| 114 | +- `spec/unit/puppet/type/` -- attribute validation, `munge`/`validate`, `autorequire` wiring. |
| 115 | +- `spec/unit/puppet/provider/<type>/` -- CRUD behaviour. Stub the external CLI/syscalls; never shell |
| 116 | + out to the real tool. Assert the *commands generated* and the *state parsed*. |
| 117 | +- **Mocking framework varies per module** -- check `spec/spec_helper.rb`'s `mock_with`. Some modules |
| 118 | + use Mocha (`stub_everything`, `stubs`, `expects`); others use rspec-mocks (`allow`, `expect`, |
| 119 | + `receive`). Match the module's existing convention. |
| 120 | + |
| 121 | +### Coverage |
| 122 | + |
| 123 | +```bash |
| 124 | +COVERAGE=yes bundle exec rspec # SimpleCov report |
| 125 | +``` |
| 126 | + |
| 127 | +- `RSpec::Puppet::Coverage.report!` runs from the PDK-managed spec helper; the minimum percentage is |
| 128 | + set in `.sync.yml` / `config_defaults.yml`. |
| 129 | +- SimpleCov `track_files` and any extra coverage config belong in `spec/spec_helper_local.rb` |
| 130 | + (`lib/**/*.rb` for ruby modules, `tasks/**/*.rb` for task modules) -- not in `spec_helper.rb`. |
| 131 | +- Do not weaken the coverage gate to make a build pass. |
| 132 | + |
| 133 | +### Litmus / beaker (acceptance) |
| 134 | + |
| 135 | +- Acceptance specs live in `spec/acceptance/` and run against Litmus-provisioned targets. They must be |
| 136 | + idempotent: apply the manifest, then assert a second apply is a no-op |
| 137 | + (`apply_manifest(pp, catch_changes: true)`). |
| 138 | +- Keep acceptance coverage on real behaviour (package installed, service running, file present) using |
| 139 | + `serverspec` matchers -- not a re-test of unit-level logic. |
| 140 | + |
| 141 | +### Spec helpers |
| 142 | + |
| 143 | +- `spec/spec_helper.rb` is **PDK-managed -- do not edit**; it is overwritten on `pdk update`. |
| 144 | +- Put project-local unit setup in `spec/spec_helper_local.rb` (safe to edit). |
| 145 | +- `spec/spec_helper_acceptance.rb` (+ `spec/spec_helper_acceptance_local.rb`) drives the Litmus |
| 146 | + acceptance run; put acceptance-only helpers there, not in the unit spec helper. |
| 147 | + |
| 148 | +--- |
| 149 | + |
| 150 | +## Ruby code style |
| 151 | + |
| 152 | +- Style is enforced by `.rubocop.yml` (deployed from pdk-templates, `strict` profile). Run |
| 153 | + `bundle exec rake rubocop` (or `bundle exec rubocop -A` to autocorrect) before pushing -- always |
| 154 | + under `bundle exec` so you get the module's pinned RuboCop version; do not hand-tune cops in the synced file. |
| 155 | +- Conventions the profile enforces: `Layout/LineLength` max 200, `snake_case` naming, `%r{}` for regex |
| 156 | + literals, trailing commas on multiline arrays/args, and the `rubocop-performance` / `rubocop-rspec` |
| 157 | + extensions. Target Ruby version follows `.rubocop.yml`'s `TargetRubyVersion`. |
| 158 | + |
| 159 | +--- |
| 160 | + |
| 161 | +## Gemfile, gem sources and version pinning |
| 162 | + |
| 163 | +- The `Gemfile` is PDK-managed. It resolves `puppet`, `facter`, `hiera` (and `bolt`) gems through a |
| 164 | + configurable source -- do not hard-code gem versions in it. |
| 165 | +- Environment knobs: `PUPPET_GEM_VERSION`, `FACTER_GEM_VERSION`, `HIERA_GEM_VERSION`, `BOLT_GEM_VERSION` |
| 166 | + pin versions; `GEM_SOURCE` overrides the default RubyGems source; `GEM_SOURCE_PUPPETCORE` (set |
| 167 | + automatically when `PUPPET_FORGE_TOKEN` is present) selects the authenticated PuppetCore registry. |
| 168 | +- The `:system_tests` group holds Litmus/serverspec; `:development` holds rspec, lint, and coverage gems. |
| 169 | +- In code and specs, depend on the public Puppet API (`Puppet::ResourceApi`, `Puppet::Type`, `Facter`), |
| 170 | + never on a specific gem name, so the module works whichever runtime gem is resolved. |
| 171 | + |
| 172 | +--- |
| 173 | + |
| 174 | +## Type & provider DSL gotchas |
| 175 | + |
| 176 | +Applies to modules that ship custom resources under `lib/puppet/`: |
| 177 | + |
| 178 | +- **`autorequire`** -- declare implicit ordering (e.g. a package before the service that uses it) in |
| 179 | + the type, rather than forcing users to write `require =>` everywhere. |
| 180 | +- **`prefetch`** -- providers listing existing resources implement `self.instances` and |
| 181 | + `self.prefetch(resources)`. `prefetch` must match discovered instances back to catalog resources by |
| 182 | + name/namevar so Puppet edits instead of recreating. Missing this causes duplicate-resource churn. |
| 183 | +- **`self.instances`** -- return only fully populated instances; partial instances break resource |
| 184 | + purging (`resources { 'x': purge => true }`). |
| 185 | +- **Idempotency** -- getters read live state, setters change it. A second run with no manifest change |
| 186 | + must report zero changes. This is the single most common review rejection. |
| 187 | +- **Resource API vs legacy** -- prefer `Puppet::ResourceApi.register_type` for new types. If a module |
| 188 | + has already migrated (check the type files), do not reintroduce the legacy `Puppet::Type.newtype` pattern. |
| 189 | +- **Boolean munging** -- Puppet booleans arrive as `:true/:false`, `true/false`, or `'true'/'false'`. |
| 190 | + Normalise explicitly before passing to a shell command. |
| 191 | +- **`commands` / `optional_commands`** -- declare required binaries with `commands` (Puppet fails at |
| 192 | + apply time if missing); use `optional_commands` for tools that may be absent. |
| 193 | + |
| 194 | +--- |
| 195 | + |
| 196 | +## Task module conventions |
| 197 | + |
| 198 | +Applies to modules whose primary interface is Bolt tasks (`tasks/`): |
| 199 | + |
| 200 | +- Each task is `<name>.{rb,sh,ps1}` plus a `<name>.json` metadata/parameter schema. |
| 201 | +- Ruby tasks use the Puppet ruby shebang `#!/opt/puppetlabs/puppet/bin/ruby` and read parameters as |
| 202 | + JSON from `$stdin`. When unit-testing, stub `$stdin`/`JSON.parse` before `require`-ing the file so |
| 203 | + the script-level dispatch block does not run on load. |
| 204 | +- Cross-platform tasks delegate to per-OS implementations (e.g. shell helpers in `files/`). |
| 205 | + |
| 206 | +--- |
| 207 | + |
| 208 | +## metadata.json conventions |
| 209 | + |
| 210 | +- `metadata.json` is the source of truth for the supported OS matrix; `on_supported_os` in specs and |
| 211 | + the CI platform list both derive from it. Update it when adding/removing platform support. |
| 212 | +- Bump `version` per [SemVer](https://semver.org): breaking change -> major, feature -> minor, fix -> patch. |
| 213 | +- Keep `dependencies` ranges current (e.g. `stdlib >= 9.0.0 < 10.0.0`); validate with `rake metadata_lint`. |
| 214 | +- Do not hand-edit machine-managed fields (`pdk-version`, `template-url`, `template-ref`). |
| 215 | +- `requirements` pins the supported Puppet range (typically `>= 8.0.0 < 9.0.0`); keep code and specs in step. |
| 216 | + |
| 217 | +--- |
| 218 | + |
| 219 | +## CI & nightly logs |
| 220 | + |
| 221 | +- CI lives in `.github/workflows/`. Many Puppetlabs modules use the shared workflows from |
| 222 | + [`puppetlabs/cat-github-actions`](https://github.com/puppetlabs/cat-github-actions) -- typically |
| 223 | + `ci.yml` (spec + acceptance on PRs to `main`), `nightly.yml` (scheduled runs against nightly |
| 224 | + builds), and `release.yml` / `release_prep.yml` -- but check what the module actually defines. |
| 225 | +- **Nightly failures:** check the module's **Actions -> "nightly"** runs on GitHub for per-job logs |
| 226 | + first. Nightlies often fail on infrastructure/provisioning rather than module code, so confirm the |
| 227 | + failure is code-related before changing anything. |
| 228 | + |
| 229 | +--- |
| 230 | + |
| 231 | +<% if has_linux -%> |
| 232 | +## Linux specifics |
| 233 | + |
| 234 | +- **Package providers**: behaviour differs by family -- `apt`/`dpkg` (Debian/Ubuntu), `yum`/`dnf`/`rpm` |
| 235 | + (RedHat/CentOS/Rocky/AlmaLinux/Oracle/Amazon), `zypper` (SLES/openSUSE). Don't assume one; gate on |
| 236 | + `$facts['os']['family']` and test each supported family. |
| 237 | +- **Service management**: modern targets are `systemd`; older ones use SysV `init`/`upstart`. Use the |
| 238 | + service provider rather than calling the init system directly. |
| 239 | +- **Paths**: Puppet ships under `/opt/puppetlabs/puppet` (ruby at `/opt/puppetlabs/puppet/bin/ruby`); |
| 240 | + config under `/etc`. Be SELinux-aware on RedHat-family targets (file contexts, `restorecon`). |
| 241 | +- **Acceptance**: Litmus provisions Linux targets as Docker containers; keep test manifests |
| 242 | + container-friendly (avoid hard reboots / kernel-level changes where possible). |
| 243 | + |
| 244 | +<% end -%> |
| 245 | +<% if has_windows -%> |
| 246 | +## Windows specifics |
| 247 | + |
| 248 | +Windows modules have a meaningfully different surface from Linux. Read these before touching |
| 249 | +iis/sqlserver/registry/scheduled_task/dsc-style modules. |
| 250 | + |
| 251 | +### Platform basics |
| 252 | + |
| 253 | +- **Package management**: Chocolatey, MSI, and the Windows installer providers -- not apt/yum. Gate |
| 254 | + provider-specific behaviour on `$facts['os']['family'] == 'windows'`. |
| 255 | +- **Services**: managed via the Windows SCM (`sc.exe` / the `service` provider), no systemd. Service |
| 256 | + names are case-insensitive; account for `LocalSystem`/`NetworkService`/named-account `logon` config. |
| 257 | +- **Paths**: Puppet installs under `C:\Program Files\Puppet Labs\Puppet` (ruby at |
| 258 | + `...\puppet\bin\ruby.exe`). Use backslash paths in resources and double them in Ruby string |
| 259 | + literals. Paths are case-insensitive and `\`-separated; normalise before comparing in providers so |
| 260 | + specs stay idempotent. CRLF line endings and BOM handling matter in file specs. |
| 261 | +- **Permissions**: there is no POSIX `mode`; model ACLs with the `puppetlabs/acl` module, not |
| 262 | + `file { mode => '0644' }`. |
| 263 | + |
| 264 | +### DSC resource patterns |
| 265 | + |
| 266 | +Applies to the generated `puppetlabs/dsc`-family modules (built from PowerShell DSC Resources): |
| 267 | + |
| 268 | +- Types and providers under `lib/puppet/type/dsc_*.rb` and `lib/puppet/provider/dsc_*` are |
| 269 | + **machine-generated by the DSC builder -- never hand-edit them**; regenerate from the upstream PS |
| 270 | + module instead. |
| 271 | +- Resource and property names are mangled: PowerShell `MyResource`/`PropertyName` become |
| 272 | + `dsc_myresource` with `dsc_propertyname` parameters (lowercased, `dsc_`-prefixed). |
| 273 | +- Embedded CIM instances (e.g. `MSFT_Credential`) are passed as hashes; credentials use |
| 274 | + `{ 'user' => ..., 'password' => Sensitive('...') }` and **must** be wrapped in `Sensitive()` so the |
| 275 | + value never lands in logs or the catalog in clear text. |
| 276 | +- Idempotency is delegated to the resource's `Test-TargetResource`; the provider invokes it through |
| 277 | + the persistent PowerShell host (ruby-pwsh). Don't add a parallel `exec` to "fix" drift a DSC |
| 278 | + resource already manages. |
| 279 | + |
| 280 | +### PowerShell exec idioms |
| 281 | + |
| 282 | +- Prefer a real type/provider or a DSC resource over shelling out. When you must run PowerShell, use |
| 283 | + the `puppetlabs/powershell` `exec` provider: `exec { 'x': provider => 'powershell', ... }`. |
| 284 | +- Always make `exec` conditional and idempotent with `onlyif`/`unless` PowerShell that sets a non-zero |
| 285 | + exit code on the "needs change" path -- never an unconditional `command`. |
| 286 | +- Providers reuse a long-lived PowerShell process via `ruby-pwsh` (`Pwsh::Manager`) for performance; |
| 287 | + prefer it over spawning `powershell.exe` per call. |
| 288 | +- Quoting cmd/PowerShell nesting is error-prone: prefer here-strings or `-EncodedCommand` (base64) for |
| 289 | + non-trivial scripts, and pass `-NonInteractive -ExecutionPolicy Bypass` so runs don't hang or get |
| 290 | + blocked by machine policy. |
| 291 | + |
| 292 | +### scheduled_task semantics |
| 293 | + |
| 294 | +Applies to the `puppetlabs/scheduled_task` type: |
| 295 | + |
| 296 | +- Default to the modern `taskscheduler_api2` provider; the legacy `win32_taskscheduler` provider |
| 297 | + exists for back-compat only -- don't reintroduce it for new work. |
| 298 | +- `trigger` is an array of hashes (`schedule`, `start_time`, `every`, `day_of_week`, ...). Most |
| 299 | + idempotency bugs come from trigger comparison: times are interpreted in the target's local timezone |
| 300 | + and round-tripped, so assert a second `apply` is a no-op in acceptance. |
| 301 | +- Set `compatibility` deliberately when a trigger type needs a newer Task Scheduler schema; a mismatch |
| 302 | + silently drops or rewrites triggers. |
| 303 | + |
| 304 | +### Native-extension Ruby gotchas |
| 305 | + |
| 306 | +- Windows providers often depend on native gems (`win32-service`, `win32-dir`, `win32-security`, |
| 307 | + `ffi`) that only load on Windows. Guard `require`s and Windows-only code paths with |
| 308 | + `Puppet.features.microsoft_windows?` so the file still loads on the Linux CI runner. |
| 309 | +- **Unit specs run on Linux** in CI even for Windows modules. Stub the Win32/FFI calls and |
| 310 | + `microsoft_windows?` rather than invoking the real API, and assert the *commands generated* and |
| 311 | + *state parsed* -- the same rule as any provider spec. |
| 312 | +- WinRM/UTF-16, CRLF, and backslash-path handling are common spec foot-guns; normalise in the code, |
| 313 | + not the test. |
| 314 | + |
| 315 | +### Litmus / acceptance on Windows |
| 316 | + |
| 317 | +- Windows targets are **not** Docker containers. Litmus provisions real VMs (vmpooler / ABS / cloud), |
| 318 | + so the Linux `litmus:provision_list[default]` Docker flow does not apply. Provision explicitly, e.g. |
| 319 | + `bundle exec rake 'litmus:provision[vmpooler, windows-2022-x86_64]'`. |
| 320 | +- The agent connects over **WinRM**, which is slower than SSH and occasionally flaky -- keep acceptance |
| 321 | + manifests tight and idempotent. |
| 322 | +- Reboots are real and sometimes required (installers, feature changes); orchestrate them with the |
| 323 | + `puppetlabs/reboot` module rather than ad-hoc `exec` restarts. |
| 324 | + |
| 325 | +<% end -%> |
| 326 | +## Review policy (outcome-based) |
| 327 | + |
| 328 | +PRs are judged on outcomes, not line counts: |
| 329 | + |
| 330 | +- **Idempotency and a clean compile across the supported OS matrix are mandatory.** A change that |
| 331 | + passes unit specs but isn't idempotent in acceptance is not done. |
| 332 | +- New behaviour ships with the test that proves it (unit for logic, acceptance for real-system effect). |
| 333 | +- Don't reduce coverage or weaken the coverage gate to make a build pass. |
| 334 | +- Keep changes scoped to the ticket; flag unrelated cleanup separately. |
| 335 | +- Reference the driving `MODULES-*` (or relevant) ticket in the PR description. |
| 336 | + |
| 337 | +--- |
| 338 | + |
| 339 | +## Project rules |
| 340 | + |
| 341 | +- At the start of a session, review the repository structure and relevant README/REFERENCE before changing anything. |
| 342 | +- Always read the files relevant to the task before suggesting or making a change. |
| 343 | +- Never merge a pull request. |
| 344 | +- Never work directly on `main` or `master`. |
| 345 | +- Never push a branch without explicit instruction. |
| 346 | +- Never delete a file without permission -- this applies even after a blanket "yes to all". |
| 347 | +- Never output, log, save, or hardcode security-sensitive values -- passwords, tokens, API keys, |
| 348 | + private keys, secrets, or credentials of any kind. Do not write them to files, commit messages, or responses. |
| 349 | + |
| 350 | +> These are guidance, not enforcement. For anything that must hold every run (secret hygiene, no |
| 351 | +> direct pushes to `main`, the coverage gate), back it with a runtime hook in the repo's `.claude/` |
| 352 | +> settings -- prose alone can be treated as a suggestion and skipped. |
0 commit comments