feat(secret-scan): gitleaks CI job + PreToolUse secret-deny hook - #117
Open
samo-agent wants to merge 3 commits into
Open
feat(secret-scan): gitleaks CI job + PreToolUse secret-deny hook#117samo-agent wants to merge 3 commits into
samo-agent wants to merge 3 commits into
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
.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.tomlextends the built-in ruleset with Cloudflarecfut_tokens; test-fixture paths are allowlisted so fake tokens in the hook test file don't self-trip the scanner.tools/hooks/secret-deny.py— a Claude Code PreToolUse hook that deniesWrite/Edittool 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/*PASSWORDvariables.tools/hooks/settings.snippet.jsonis the manager-review snippet — it is NOT auto-applied to any live settings.dd65d0fcontains 17 failing pipe-tests (hook file absent); GREEN commitf27196aadds 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 commitstep in the first place.How a planted fake secret would trip the gitleaks job
GH_TOKEN=ghp_TestTokenABCDEFGHIJKLMNOPQRSTUVWXYZ12(36 alphanum afterghp_).github-patgitleaks rule fires; the job exits non-zero; the CI check fails with an inline annotation on the offending line..gitleaks.tomlallowlist entrycfut_fake/ghp_fake/ etc. ensures test-fixture strings intest/secret-deny-hook.test.tsare exempted — real tokens (without thefakeliteral) are still caught.Pipe-test results (smoke evidence)
Writewithcfut_realtoken1234567890abc{"permissionDecision":"deny","reason":"Secret token prefix detected: 'cfut_realt'..."}Writewith-----BEGIN RSA PRIVATE KEY-----{"permissionDecision":"deny","reason":"PEM private-key block detected..."}Editnew_stringwithDB_PASSWORD = "xK9m..."(entropy=4.95){"permissionDecision":"deny","reason":"High-entropy value (entropy=4.95 bits, len=31)..."}Writewith clean README prose{"permissionDecision":"allow"}Bashtool with token-like string{"permissionDecision":"allow"}Client deployment notes
Gitleaks CI job — copy
.github/workflows/secret-scan.ymland.gitleaks.tomlverbatim into the client repo. The persona'sengineerstep 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; noGITLEAKS_LICENSEneeded).PreToolUse hook — the persona's Claude Code settings file (managed by the samo-agent bootstrap) should merge the
hooks.PreToolUseblock fromtools/hooks/settings.snippet.json, with the path pointing to the deployed copy ofsecret-deny.py. This applies the deny guard to everyWrite/Editcall the persona makes, regardless of which client project it is working on.What is intentionally NOT in this MR
~/.claude/settings.json— manager reviews the snippet and applies manually.🤖 Generated with Claude Code