Skip to content

feat(secret-scan): gitleaks CI job + PreToolUse secret-deny hook - #117

Open
samo-agent wants to merge 3 commits into
NikolayS:mainfrom
samo-agent:feat/enforce-secret-scan
Open

feat(secret-scan): gitleaks CI job + PreToolUse secret-deny hook#117
samo-agent wants to merge 3 commits into
NikolayS:mainfrom
samo-agent:feat/enforce-secret-scan

Conversation

@samo-agent

Copy link
Copy Markdown
Collaborator

Summary

  • Deliverable 1: .github/workflows/secret-scan.yml — a dedicated gitleaks job that runs on every PR and push to main, fails hard on any secret hit. .gitleaks.toml extends the built-in ruleset with Cloudflare cfut_ tokens; test-fixture paths are allowlisted so fake tokens in the hook test file don't self-trip the scanner.
  • Deliverable 2: tools/hooks/secret-deny.py — a Claude Code PreToolUse hook that denies Write/Edit tool calls whose content contains known token prefixes (cfut_ ghp_ glpat- sk- AKIA), PEM private-key headers, or high-entropy (>3.5 bits Shannon entropy) values assigned to *TOKEN/*KEY/*SECRET/*PASSWORD variables. tools/hooks/settings.snippet.json is the manager-review snippet — it is NOT auto-applied to any live settings.
  • TDD: RED commit dd65d0f contains 17 failing pipe-tests (hook file absent); GREEN commit f27196a adds all artifacts → 17/17 pass, full suite 1278/1278.

Root-cause (incident reference)

A Cloudflare token leaked into an agent log this session. Both deliverables address two distinct failure modes: CI catches secrets already committed; the hook prevents them ever reaching a git commit step in the first place.

How a planted fake secret would trip the gitleaks job

  1. Add a file anywhere in the repo containing e.g. GH_TOKEN=ghp_TestTokenABCDEFGHIJKLMNOPQRSTUVWXYZ12 (36 alphanum after ghp_).
  2. The built-in github-pat gitleaks rule fires; the job exits non-zero; the CI check fails with an inline annotation on the offending line.
  3. The secret must be removed (or the commit squashed/reverted) before the check can pass.
  4. The .gitleaks.toml allowlist entry cfut_fake / ghp_fake / etc. ensures test-fixture strings in test/secret-deny-hook.test.ts are exempted — real tokens (without the fake literal) are still caught.

Pipe-test results (smoke evidence)

Payload Expected Result
Write with cfut_realtoken1234567890abc deny {"permissionDecision":"deny","reason":"Secret token prefix detected: 'cfut_realt'..."}
Write with -----BEGIN RSA PRIVATE KEY----- deny {"permissionDecision":"deny","reason":"PEM private-key block detected..."}
Edit new_string with DB_PASSWORD = "xK9m..." (entropy=4.95) deny {"permissionDecision":"deny","reason":"High-entropy value (entropy=4.95 bits, len=31)..."}
Write with clean README prose allow {"permissionDecision":"allow"}
Bash tool with token-like string allow {"permissionDecision":"allow"}

Client deployment notes

Gitleaks CI job — copy .github/workflows/secret-scan.yml and .gitleaks.toml verbatim into the client repo. The persona's engineer step should include this copy in its first commit on any new client repo it bootstraps. No secrets or env vars required (public repo, personal account; no GITLEAKS_LICENSE needed).

PreToolUse hook — the persona's Claude Code settings file (managed by the samo-agent bootstrap) should merge the hooks.PreToolUse block from tools/hooks/settings.snippet.json, with the path pointing to the deployed copy of secret-deny.py. This applies the deny guard to every Write/Edit call the persona makes, regardless of which client project it is working on.

What is intentionally NOT in this MR

  • Branch protection rule changes (flagged for Nik, governance decision).
  • Auto-application of the hook to any live ~/.claude/settings.json — manager reviews the snippet and applies manually.

🤖 Generated with Claude Code

samo-agent and others added 3 commits July 3, 2026 15:41
Pins the required behavior of the PreToolUse secret-deny hook before
implementation: deny Write/Edit containing cfut_/ghp_/glpat-/sk-/AKIA
prefixes, PEM private-key headers, or high-entropy *TOKEN|*KEY|*SECRET|
*PASSWORD assignments; allow everything else and all non-Write/Edit tools.

17 tests fail because tools/hooks/secret-deny.py does not exist yet.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Deliverable 1 — .github/workflows/secret-scan.yml
  Uses gitleaks/gitleaks-action@v2 with full fetch-depth so every commit
  in a PR diff is scanned.  Job runs on PR and push-to-main, fails on any
  hit.  .gitleaks.toml extends the built-in ruleset with cfut_ and samo_
  token patterns; allowlists the hook test fixture file by path.

Deliverable 2 — tools/hooks/secret-deny.py + tools/hooks/settings.snippet.json
  PreToolUse hook that denies Write/Edit tool calls containing:
    - Token prefixes: cfut_ ghp_ glpat- sk- AKIA
    - PEM private-key headers
    - High-entropy (>3.5 bits) values in *TOKEN/*KEY/*SECRET/*PASSWORD vars
  Only new_string is inspected for Edit (old_string is already in the repo).
  settings.snippet.json is a review-only artifact — not auto-applied.

All 17 pipe-tests green; full suite 1278/1278.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Unused import caused bunx tsc --noEmit to exit 2 in CI.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
samo-agent added a commit to samo-agent/samohost that referenced this pull request Jul 7, 2026
Adds the permanent, replicable client-onboarding package:

templates/client-repo/ (versioned in samohost):
- .github/workflows/ci.yml  — generic client CI using a postgres:18 service
  container; parameterised runner tag ({{RUNNER_TAG}}); NO field-record-1 NikolayS#117
  AppArmor/host-PG workarounds
- .samohost.toml             — standard client manifest template with all
  canonical fields (name, repo, branch, appDir, buildCmd, migrateCmd,
  healthUrl, serviceUnit, kind, dbBackend, mainHost)
- CLAUDE.md                  — carries SAMO-STACK:START/END + SAMO-DEV-PRINCIPLES
  sync-block markers so client-project agents load principles + stack docs
- staging.env.example        — standard env var shape documentation

src/commands/onboard.ts:
- runOnboard() — scaffolds templates/client-repo/ into the target repo as
  a branch + PR (samo-agent identity via GITHUB_TOKEN), then calls
  runAppRegister() to register the app in state, verifies trigger coverage
  (app appears in appStore = trigger picks it up). Fully idempotent: re-run
  updates files on existing branch, surfaces existing PR URL, upserts app
  record without duplication.
- renderTemplate() — {{RUNNER_TAG}}/{{APP_NAME}}/{{REPO}}/{{APP_DB_NAME}}
  substitution with sensible defaults
- TEMPLATE_FILES constant — canonical list of scaffolded paths
- defaultOnboardDeps() — live GitHub API deps (requires GITHUB_TOKEN)

src/cli.ts:
- parseArgs onboard <org/repo> --vm <name> [--runner-tag <tag>]
  [--toml-path <path>]
- Dispatch case wired into main()

All 1374 tests pass, typecheck clean.

Deferred (not built now, per brief):
- Mechanising per-repo self-hosted runner registration (docs/runner-admin-handoff.md PAT flow)
- field-record cutover onto the canonical path

Closes NikolayS#127 (the package; rollout checklist in the issue remains open)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
samo-agent pushed a commit to samo-agent/samohost that referenced this pull request Jul 7, 2026
Pins the expected behaviour for all 5 blockers from the samorev REQUEST_CHANGES:

1. EXIT-CODE HONESTY — test/onboard-exit-code.test.ts: imports
   `computeOnboardExitCode` (does not exist yet) and asserts it returns 1 for
   every partial-failure shape (appRegistered=false, triggerCovered=false,
   status="error") and 0 only on full success.

2. REAL CI.YML TEST — replaces the Array.isArray placeholder at test line 400
   with a real readFileSync of the template and checks for absence of NikolayS#117,
   AppArmor, and host-PG markers.

3. CI.YML COMMENT — asserts the template comment does NOT say "repository
   variable" (it is a scaffold-time substitution, not a repo variable).

4. CI.YML DUPLICATE RUNS-ON — asserts the rendered default (RUNNER_TAG=
   "self-hosted") does not produce [self-hosted, "self-hosted"].

5. RE-ONBOARD CLOBBER GUARD — asserts scaffoldFile is never called for
   .samohost.toml when the repo already has one.

6. findPr ERROR SURFACING — asserts defaultOnboardDeps().findPr throws on
   non-200 HTTP instead of silently returning null.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
samo-agent pushed a commit to samo-agent/samohost that referenced this pull request Jul 7, 2026
…ikolayS#131)

Root-cause fixes (not symptom guards):

1. EXIT-CODE HONESTY — cli.ts: Extract `computeOnboardExitCode(report)` and
   wire it into the onboard case.  Returns 1 when `status==="error"` OR
   `appRegistered===false` OR `triggerCovered===false`.  Previously the
   inline code only checked `status==="error"`, silently returning 0 when
   the PR was created but the app was not registered.

2. REAL CI.YML TEST (test-only) — test/onboard.test.ts: the placeholder
   `Array.isArray(TEMPLATE_FILES)` check at line 400 is replaced with a real
   `readFileSync` of templates/client-repo/.github/workflows/ci.yml asserting
   absence of `NikolayS#117`, `AppArmor`, and `host-PG` markers.

3. CI.YML TEMPLATE — templates/client-repo/.github/workflows/ci.yml:
   (a) Corrected the runs-on comment: RUNNER_TAG is a scaffold-time
       {{RUNNER_TAG}} substitution (performed by `samohost onboard`), NOT a
       GitHub repository variable.
   (b) Changed runs-on from `[self-hosted, "{{RUNNER_TAG}}"]` to
       `"{{RUNNER_TAG}}"`.  The old form rendered `[self-hosted, "self-hosted"]`
       (duplicate label) when the default "self-hosted" was used.

4. RE-ONBOARD CLOBBER GUARD — src/commands/onboard.ts: Track whether
   `.samohost.toml` was fetched from the target repo (vs. generated from the
   template).  If it was, skip `scaffoldFile` for `.samohost.toml` in the
   scaffold loop.  The real manifest is still used for app registration;
   only the overwrite is suppressed.

5. findPr ERROR SURFACING — defaultOnboardDeps().findPr now throws
   `Error("GitHub API error ${status} ...")` on non-200 HTTP instead of
   silently returning null.  A null return incorrectly routed to createPr,
   risking duplicate PRs on transient API failures.

All 1382 tests pass; `bunx tsc --noEmit` clean.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
samo-agent added a commit that referenced this pull request Jul 7, 2026
…ard command (#127) (#131)

* test(onboard): RED — unit tests for client-onboard-package command

Adds failing tests for:
- parseArgs onboard <org/repo> --vm <name> parsing + error cases
- runOnboard scaffold shape (all TEMPLATE_FILES, CLAUDE.md sync markers,
  staging.env.example, PR URL in report)
- runOnboard app registration and trigger coverage verification
- runOnboard idempotency (re-run → updated, no duplicate PR or app record)
- renderTemplate {{RUNNER_TAG}} substitution
- TEMPLATE_FILES exported constant

All tests fail with "Cannot find module '../src/commands/onboard.ts'" —
the module does not exist yet (this is the RED commit).

Refs #127

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(onboard): GREEN — client-onboarding package (issue #127)

Adds the permanent, replicable client-onboarding package:

templates/client-repo/ (versioned in samohost):
- .github/workflows/ci.yml  — generic client CI using a postgres:18 service
  container; parameterised runner tag ({{RUNNER_TAG}}); NO field-record-1 #117
  AppArmor/host-PG workarounds
- .samohost.toml             — standard client manifest template with all
  canonical fields (name, repo, branch, appDir, buildCmd, migrateCmd,
  healthUrl, serviceUnit, kind, dbBackend, mainHost)
- CLAUDE.md                  — carries SAMO-STACK:START/END + SAMO-DEV-PRINCIPLES
  sync-block markers so client-project agents load principles + stack docs
- staging.env.example        — standard env var shape documentation

src/commands/onboard.ts:
- runOnboard() — scaffolds templates/client-repo/ into the target repo as
  a branch + PR (samo-agent identity via GITHUB_TOKEN), then calls
  runAppRegister() to register the app in state, verifies trigger coverage
  (app appears in appStore = trigger picks it up). Fully idempotent: re-run
  updates files on existing branch, surfaces existing PR URL, upserts app
  record without duplication.
- renderTemplate() — {{RUNNER_TAG}}/{{APP_NAME}}/{{REPO}}/{{APP_DB_NAME}}
  substitution with sensible defaults
- TEMPLATE_FILES constant — canonical list of scaffolded paths
- defaultOnboardDeps() — live GitHub API deps (requires GITHUB_TOKEN)

src/cli.ts:
- parseArgs onboard <org/repo> --vm <name> [--runner-tag <tag>]
  [--toml-path <path>]
- Dispatch case wired into main()

All 1374 tests pass, typecheck clean.

Deferred (not built now, per brief):
- Mechanising per-repo self-hosted runner registration (docs/runner-admin-handoff.md PAT flow)
- field-record cutover onto the canonical path

Closes #127 (the package; rollout checklist in the issue remains open)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(onboard/RED): 5 failing tests for samorev blockers (#131)

Pins the expected behaviour for all 5 blockers from the samorev REQUEST_CHANGES:

1. EXIT-CODE HONESTY — test/onboard-exit-code.test.ts: imports
   `computeOnboardExitCode` (does not exist yet) and asserts it returns 1 for
   every partial-failure shape (appRegistered=false, triggerCovered=false,
   status="error") and 0 only on full success.

2. REAL CI.YML TEST — replaces the Array.isArray placeholder at test line 400
   with a real readFileSync of the template and checks for absence of #117,
   AppArmor, and host-PG markers.

3. CI.YML COMMENT — asserts the template comment does NOT say "repository
   variable" (it is a scaffold-time substitution, not a repo variable).

4. CI.YML DUPLICATE RUNS-ON — asserts the rendered default (RUNNER_TAG=
   "self-hosted") does not produce [self-hosted, "self-hosted"].

5. RE-ONBOARD CLOBBER GUARD — asserts scaffoldFile is never called for
   .samohost.toml when the repo already has one.

6. findPr ERROR SURFACING — asserts defaultOnboardDeps().findPr throws on
   non-200 HTTP instead of silently returning null.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(onboard/GREEN): address all 5 samorev REQUEST_CHANGES blockers (#131)

Root-cause fixes (not symptom guards):

1. EXIT-CODE HONESTY — cli.ts: Extract `computeOnboardExitCode(report)` and
   wire it into the onboard case.  Returns 1 when `status==="error"` OR
   `appRegistered===false` OR `triggerCovered===false`.  Previously the
   inline code only checked `status==="error"`, silently returning 0 when
   the PR was created but the app was not registered.

2. REAL CI.YML TEST (test-only) — test/onboard.test.ts: the placeholder
   `Array.isArray(TEMPLATE_FILES)` check at line 400 is replaced with a real
   `readFileSync` of templates/client-repo/.github/workflows/ci.yml asserting
   absence of `#117`, `AppArmor`, and `host-PG` markers.

3. CI.YML TEMPLATE — templates/client-repo/.github/workflows/ci.yml:
   (a) Corrected the runs-on comment: RUNNER_TAG is a scaffold-time
       {{RUNNER_TAG}} substitution (performed by `samohost onboard`), NOT a
       GitHub repository variable.
   (b) Changed runs-on from `[self-hosted, "{{RUNNER_TAG}}"]` to
       `"{{RUNNER_TAG}}"`.  The old form rendered `[self-hosted, "self-hosted"]`
       (duplicate label) when the default "self-hosted" was used.

4. RE-ONBOARD CLOBBER GUARD — src/commands/onboard.ts: Track whether
   `.samohost.toml` was fetched from the target repo (vs. generated from the
   template).  If it was, skip `scaffoldFile` for `.samohost.toml` in the
   scaffold loop.  The real manifest is still used for app registration;
   only the overwrite is suppressed.

5. findPr ERROR SURFACING — defaultOnboardDeps().findPr now throws
   `Error("GitHub API error ${status} ...")` on non-200 HTTP instead of
   silently returning null.  A null return incorrectly routed to createPr,
   risking duplicate PRs on transient API failures.

All 1382 tests pass; `bunx tsc --noEmit` clean.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: samo-agent <samo-agent@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: samo-agent <samo-agent@github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant