Skip to content

Posture Profiles

A posture profile is a named bundle of desired-state security intents — branch protection, required checks, signed-commit enforcement, and repository settings — that you can apply to any repository with a single command. Profiles generalise the koryph repo check|apply workflow: instead of writing forge-native IaC files yourself, you pick a profile and pass parameters.

Koryph ships one built-in profile: oss-solo-maintainer. You can also create custom profiles in ~/.koryph/postures/<name>/.

!!! note "Forge coverage" Posture profiles are forge-neutral intents. On GitHub (the reference forge) they compile to rulesets + repository-settings JSON under .github/; on GitLab they compile to protected branches, push rules, and approval rules. This chapter's examples show the GitHub compilation. See Choosing a forge for the per-forge capability table.


Built-in profile: oss-solo-maintainer

Designed for an open-source project with a single (or small team of) maintainer(s). Applies:

Section What it sets
pr-checks ruleset 1 required review, optional required CI check names (via --param)
signed-commits ruleset required_signatures, non_fast_forward, deletion on the default branch
Repo settings allow_squash_merge=true, allow_merge_commit=false, delete_branch_on_merge=true, web_commit_signoff_required=true
Security & analysis secret_scanning, secret_scanning_push_protection, dependabot_security_updates all enabled
Vulnerability alerts enabled
Actions workflow default_workflow_permissions=read, can_approve_pull_request_reviews=true

Parameters

Name Description Default
required_checks Comma-separated required CI check names added to the pr-checks ruleset. Omit to skip the required_status_checks rule entirely. (empty — rule omitted)

Commands

All posture commands accept --repo owner/name. When --repo is omitted, koryph detects the repository from the current directory's git remote via gh.

posture list

koryph posture list

Lists all available profiles — built-ins (embedded in the binary) and user profiles in ~/.koryph/postures/.

NAME                  SOURCE    DESCRIPTION
oss-solo-maintainer   builtin   Baseline posture for an OSS project with a solo maintainer: ...
my-custom-profile     user      My company standard posture

posture describe

koryph posture describe <profile> [--repo owner/name] [--param k=v]...

Renders a human-readable explanation of every managed setting and ruleset rule in the profile:

  • Target value — what the profile enforces for each setting key.
  • Security rationale — plain-language explanation of what attack or mistake the setting prevents (e.g. signed commits = commit provenance; push protection = credential leak prevention; 1 required review = no unreviewed changes on main).
  • Live value and change status (with --repo) — the current GitHub value for each setting, and whether applying the profile would change it.

Rationale text is sourced from three places, in order of precedence: 1. The --param-derived manifest descriptions map (community profiles may ship this in manifest.json). 2. The descriptions map in the profile's repo-settings.json file. 3. Built-in fallback rationale in koryph for all well-known setting keys and rule types.

Ruleset files may also carry _rationale (a per-ruleset summary) and _rule_descriptions (a per-rule map) at the top level of their JSON — these fields are stripped during check/apply normalization and are purely informational.

Profile-only description (no live check):

koryph posture describe oss-solo-maintainer

Sample output (truncated):

Profile: oss-solo-maintainer
Baseline posture for an OSS project with a solo maintainer: ...

── Repo Settings ───────────────────────────────────────────────────────────────

  [repo flags]
  allow_merge_commit                         false
    Prevents merge commits on the default branch, enforcing a clean bisectable
    history where every change is squash-merged or rebase-merged.

  ...

── Rulesets ────────────────────────────────────────────────────────────────────

  [signed-commits] target: branch  conditions: ~DEFAULT_BRANCH
  Enforces cryptographic commit signing and protects default-branch integrity ...

    required_signatures
      All commits must be GPG or SSH signed, proving commit provenance and
      making unauthorized history modifications detectable.

    non_fast_forward
      Prevents force-pushes that rewrite history on the protected branch.

    deletion
      Prevents accidental or malicious deletion of the protected branch.

With live comparison (--repo):

koryph posture describe oss-solo-maintainer --repo myorg/myrepo \
  --param required_checks="pre-commit,make gate"

Each setting line gains a live: sub-line:

  secret_scanning                            "enabled"
    live: "disabled"                              [→ WOULD CHANGE]

posture check

koryph posture check <profile> [--repo owner/name] [--param k=v]... [--no-fail]

Compares the live GitHub repository state against the profile. Prints OK, MISSING, or DRIFT per section — identical to koryph repo check. Exits 1 if drift is detected (useful for CI gating); pass --no-fail to always exit 0 (informational — useful for exploration and auditing without breaking scripts).

koryph posture check oss-solo-maintainer --repo myorg/myrepo \
  --param required_checks="pre-commit,make gate"

posture diff (deprecated alias)

koryph posture diff <profile> [--repo owner/name] [--param k=v]...

Deprecated alias for posture check --no-fail — same behavior, always exits 0. Prefer check --no-fail in new scripts.

posture apply

koryph posture apply <profile> [--repo owner/name] [--param k=v]...

Prints the diff between the live state and the profile, then applies any changes. Never deletes rulesets it does not know about.

Before making any live change, koryph captures the current GitHub state into a timestamped snapshot under <repo-root>/.koryph/snapshots/settings-<ts>.json. If the diff is empty (nothing to change), no snapshot is written. Roll back with koryph posture rollback (see below) — an alias for koryph repo rollback.

koryph posture apply oss-solo-maintainer --repo myorg/myrepo \
  --param required_checks="pre-commit,make gate"

Output:

--- rulesets diff ---
MISSING  pr-checks (no live ruleset)
MISSING  signed-commits (no live ruleset)
captured pre-change state → .koryph/snapshots/settings-2026-07-04T16-40-18Z.json; rollback with koryph posture rollback
--- applying rulesets ---
CREATED  pr-checks
CREATED  signed-commits
--- settings diff ---
DRIFT    security & analysis:
         - {"dependabot_security_updates":"disabled","secret_scanning":"disabled",...}
         + {"dependabot_security_updates":"enabled","secret_scanning":"enabled",...}
--- applying settings ---
UPDATED  security & analysis

repo describe — describe repo-local IaC

koryph repo describe produces the same output format as posture describe but reads from the repository's own .github/ IaC files instead of a named profile:

koryph repo describe [--repo owner/name]

Use it to understand what a repository's own .github/rulesets/*.json and .github/repo-settings.json enforce and why, with the same per-setting rationale as posture describe. The --repo flag adds live values and change markers identical to those in posture describe --repo.


Ejectability — repo-local .github/ overrides the profile

A repository that has ejected from a profile by writing its own .github/ IaC files stays sovereign. Koryph detects this automatically, per section:

  • If .github/rulesets/ exists in the current directory → local rulesets win; profile rulesets are ignored.
  • If .github/repo-settings.json exists → local settings win; profile settings are ignored.

Koryph prints an INFO line for each overridden section:

INFO     rulesets: repo has .github/rulesets/ — using local IaC (profile rulesets ignored)

This means you can safely run koryph posture check oss-solo-maintainer in any repo — repos that have their own IaC are unaffected. Ejected repos continue to be managed by koryph repo check|apply.


Solo-maintainer walkthrough (zero to compliant)

This walkthrough shows how to apply the oss-solo-maintainer profile to a new GitHub repository. You need gh authenticated and koryph on your PATH.

1. Check current drift:

koryph posture check oss-solo-maintainer --repo myorg/myrepo \
  --param required_checks="pre-commit,ci"

Expect MISSING lines for the rulesets and DRIFT lines for security settings on a freshly created repo.

2. Apply the profile:

koryph posture apply oss-solo-maintainer --repo myorg/myrepo \
  --param required_checks="pre-commit,ci"

Koryph prints the diff, then creates the two rulesets and patches the settings.

3. Verify no remaining drift:

koryph posture check oss-solo-maintainer --repo myorg/myrepo \
  --param required_checks="pre-commit,ci"
# exits 0 — OK for every section

4. Ongoing enforcement (optional):

Add the check to CI (e.g. a scheduled GitHub Actions workflow):

- name: posture check
  run: |
    koryph posture check oss-solo-maintainer \
      --param required_checks="pre-commit,make gate"

Profile architecture: intents vs. native passthrough

Posture profiles support two complementary authoring styles:

Intents (forge-neutral)

An intents block in manifest.json describes what the profile enforces in forge-agnostic terms. Koryph compiles intents to native controls for the active forge (currently GitHub). The same intent vocabulary will apply to GitLab and other forges without changes to your profile.

{
  "name": "my-company",
  "description": "Company-standard posture",
  "intents": {
    "require_approvals": 2,
    "require_signed_commits": true,
    "no_force_push": true,
    "no_deletion": true,
    "secret_scanning": true,
    "secret_scanning_push_protection": true,
    "dependabot_security_updates": true,
    "vulnerability_alerts": true,
    "allow_merge_commit": false,
    "allow_squash_merge": true,
    "allow_rebase_merge": false,
    "allow_auto_merge": false,
    "delete_branch_on_merge": true,
    "allow_update_branch": true,
    "web_commit_signoff_required": true,
    "actions_default_permissions": "read",
    "actions_can_approve_prs": false
  }
}

Intent fields (all optional — omit fields the profile does not manage):

Field Type GitHub target
require_approvals int (0 = none) pr-checks ruleset required_approving_review_count
required_checks string[] pr-checks ruleset required_status_checks
require_signed_commits bool signed-commits ruleset required_signatures
no_force_push bool signed-commits ruleset non_fast_forward
no_deletion bool signed-commits ruleset deletion
secret_scanning bool repo security — secret_scanning: enabled
secret_scanning_push_protection bool repo security — secret_scanning_push_protection: enabled
dependabot_security_updates bool repo security — dependabot_security_updates: enabled
vulnerability_alerts bool vulnerability alerts enabled
allow_merge_commit bool|null repo.allow_merge_commit
allow_squash_merge bool|null repo.allow_squash_merge
allow_rebase_merge bool|null repo.allow_rebase_merge
allow_auto_merge bool|null repo.allow_auto_merge
delete_branch_on_merge bool repo.delete_branch_on_merge
allow_update_branch bool repo.allow_update_branch
web_commit_signoff_required bool repo.web_commit_signoff_required
actions_default_permissions "read" or "write" actions workflow permissions
actions_can_approve_prs bool actions workflow can approve PRs

required_checks in the intents block can also be supplied (or overridden) at runtime via --param required_checks=....

Native passthrough (forge-specific escape hatch)

When you need forge-specific controls that have no intent equivalent, place them in a forge subdirectory inside the profile:

~/.koryph/postures/my-company/
  manifest.json          ← intents block (forge-neutral)
  github/                ← applied verbatim on GitHub only
    rulesets/
      extra-protection.json

Files in github/ are copied to .github/ verbatim (no template rendering) on top of the compiled intent output. They are marked [non-portable: github-native] in posture describe output to make the forge coupling visible.

Repo-local .github/ IaC continues to override profile output entirely (ejectability is unchanged).

Legacy file-based profiles

Profiles without an intents block use the original file-tree layout (raw JSON / JSON template files in the profile root). This mode is still fully supported for backward compatibility with existing user profiles.

~/.koryph/postures/my-company/
  manifest.json
  rulesets/
    main-protection.json
    signed-commits.json
  repo-settings.json

Template files (suffix .json.tmpl) are rendered with Go text/template. Available variables:

Variable Description
.RequiredChecks Slice of {Context string} objects for required CI checks

Use {{toJSON .RequiredChecks}} to emit a JSON array of {"context":"…"} objects.

Static files (.json, no .tmpl suffix) are copied verbatim.


Creating a custom profile

A user profile lives at ~/.koryph/postures/<name>/. Its structure depends on the authoring style you choose (see above).

Intents-based (recommended for new profiles):

~/.koryph/postures/my-company/
  manifest.json    ← with "intents" block
  github/          ← optional native passthrough
    rulesets/
      extra.json

Legacy file-based:

~/.koryph/postures/my-company/
  manifest.json
  rulesets/
    main-protection.json
    signed-commits.json
  repo-settings.json

manifest.json example (legacy style):

{
  "name": "my-company",
  "description": "Company-standard GitHub posture",
  "parameters": {
    "required_checks": {
      "description": "Comma-separated required CI check names",
      "default": "build,test"
    }
  }
}

User profiles take precedence over built-ins of the same name — you can override oss-solo-maintainer by creating ~/.koryph/postures/oss-solo-maintainer/.

Making a custom profile self-documenting

Add a descriptions map to manifest.json to override or add rationale for any setting key. Built-in rationale already exists for all keys used by oss-solo-maintainer; override when your profile sets keys with different intent or adds novel keys:

{
  "name": "my-company",
  "description": "Company-standard GitHub posture",
  "descriptions": {
    "allow_auto_merge": "Auto-merge is enabled for our release bot (override: intentional).",
    "my_custom_key":   "Explanation of what this company-specific setting prevents."
  }
}

Alternatively, add a descriptions map directly in repo-settings.json:

{
  "repo": { ... },
  "descriptions": {
    "allow_merge_commit": "Custom rationale next to the setting it describes."
  }
}

For rulesets, add _rationale (per-ruleset summary) and _rule_descriptions (per-rule map) to the ruleset JSON file — these fields are stripped during check/apply normalization and are purely informational:

{
  "name": "my-protection",
  "_rationale": "Enforces our branch protection policy.",
  "_rule_descriptions": {
    "deletion": "Prevents accidental branch deletion by our CI bots."
  },
  "enforcement": "active",
  "target": "branch",
  "rules": [
    { "type": "deletion" }
  ]
}

Rationale lookup order: _rule_descriptions in the file > manifest.json descriptions (keyed as "rule.<type>") > built-in fallback.


Pre-apply snapshots and rollback

Every koryph repo apply and koryph posture apply that would change live settings first captures the current live state into a timestamped snapshot:

<repo-root>/.koryph/snapshots/settings-2026-07-04T16-40-18Z.json

The snapshot schema:

{
  "captured_at": "2026-07-04T16:40:18Z",
  "repo": "owner/name",
  "applied_profile": "oss-solo-maintainer",
  "sections": {
    "repo_flags": { "description": "...", "allow_squash_merge": true, "..." : "..." },
    "security_and_analysis": { "secret_scanning": "enabled", "..." : "..." },
    "vulnerability_alerts": true,
    "actions_workflow_permissions": { "default_workflow_permissions": "read", "..." : "..." },
    "rulesets": {
      "protect-main": { "name": "protect-main", "..." : "..." }
    }
  }
}

For koryph repo apply the top-level key is "iac" instead of "applied_profile". Snapshots contain observed repo config — no secrets.

Snapshots are gitignored by default. Koryph writes .koryph/snapshots/ into the project's .gitignore automatically (idempotent, appended if missing) the first time a snapshot is created, and again during koryph project add. Do not commit snapshot files — they are machine-local state.

When the diff is empty (nothing would change) no snapshot is written.

repo rollback

koryph repo rollback [--repo owner/name] [--to <timestamp>|latest]

koryph posture rollback is an identical alias — use whichever spelling matches how you got here (repo apply/repo check vs. posture apply).

Lists the available snapshots when no --to is given and multiple exist for the repo. Shows a diff of snapshot vs. live state before applying (same diff-first discipline as apply). If the live state already matches the snapshot, prints "no drift" and exits without changing anything.

# Roll back to the most recent snapshot:
koryph repo rollback

# Roll back to a specific snapshot by exact or prefix timestamp:
koryph repo rollback --to 2026-07-04T16:40:18Z
koryph repo rollback --to 2026-07-04T16          # must resolve to exactly one snapshot

# Specify a repo explicitly:
koryph repo rollback --repo myorg/myrepo --to latest

Rollback applies the snapshot through the same apply machinery — it is idempotent and safe to run repeatedly. Snapshots are never deleted automatically; clean them up manually when you no longer need them.