Skip to content

fix(security): hook-log redaction + postinstall SHA pinning (wish G3+G4) - #1664

Merged
namastex888 merged 4 commits into
devfrom
wish/dep-hygiene-and-resilience
May 5, 2026
Merged

namastex888 merged 4 commits into
devfrom
wish/dep-hygiene-and-resilience

Conversation

@namastex888

Copy link
Copy Markdown
Contributor

Wish: dep-hygiene-and-resilience — Groups 3 and 4 of 8.

Summary

Two security-critical groups from the dep-hygiene wish. Both close credential / supply-chain failure modes that were live on the box today.

  • G3 — Hook-fallback log security. Sentinel finding 2026-05-05: ~/.genie/hook-fallback.log was world-readable with full bash command lines including a token-bearing gh pr create. Tighten to mode 0600 at create-time + first-write migration; redact gh[ps]_…, sk-…, glpat-…, and 40+ hex shapes at write time.
  • G4 — Postinstall SHA-256 pinning. Same code path that produced empty bin/bun today is the path a tarball-swap attack would exploit. Pin-or-fail: download → verify SHA-256 against package.json#binarySha256 → only then write to disk and chmod +x. Mismatch aborts install with a clear error.

The remaining six groups (G1 pgserve PATH fallback, G2 TUI degrade panel, G5 pm2 lifecycle, G6 doctor probes, G7 dep audit, G8 QA) are tracked in .genie/wishes/dep-hygiene-and-resilience/STATUS.md.

What's in this PR

Change Group
src/hooks/redaction.ts (new) — token-shape redaction module G3
src/hooks/__tests__/redaction.test.ts (new) — 12 tests G3
src/hooks/dispatch-client.ts — ensureLogPermissions() (chmod 0600 with one-time migration), redact record.command at write, pass { mode: 0o600 } to all appendFileSync/writeFileSync G3
package.json — binarySha256 block with real upstream SHAs for tmux 3.6a (linux x86_64/arm64, macos x86_64/arm64) G4
scripts/postinstall-tmux.js — getPinnedSha() resolves package.json, verifies SHA-256 in-memory before writing, aborts on mismatch / missing key G4
scripts/postinstall-hook-binary.js — local-compile path is exempt from pinning but now logs the entry source path so operators can verify by other means G4
.github/workflows/binary-sha-drift.yml — re-fetches each pinned asset on every PR touching package.json or postinstall scripts; surfaces drift as a deliberate diff comment G4
.genie/wishes/dep-hygiene-and-resilience/binary-sha-bootstrap.md — bootstrap procedure and failure-mode table (lives inside the wish dir until the docs vendor submodule lands the canonical copy) G4
.genie/wishes/dep-hygiene-and-resilience/STATUS.md — tracks shipped vs pending groups + open /review MEDIUMs wish hygiene

Validation

  • bun test src/hooks/__tests__/redaction.test.ts → 12/12 pass (50 expect calls).
  • bun test src/hooks/__tests__/dispatch.test.ts → 19/19 pass (regression guard for G3 chmod / redaction wiring).
  • bun run typecheck → clean.
  • bunx biome check → clean across all G3+G4 files.
  • Manual SHA tamper-test (G4): matching pin passes; appending a single byte produces a different SHA and the script aborts with Error: <asset> SHA-256 mismatch — expected <pinned>, got <actual>. Aborting install.

Test plan

  • CI green (typecheck, lint, dead-code, skills-lint, wishes-lint, lint-emit-discipline, bun test).
  • Drift workflow green (re-fetches pinned assets, confirms SHA match).
  • On a host where ~/.genie/hook-fallback.log exists with looser perms, observe the one-line stderr migration notice on first write after upgrade and confirm ls -l reports -rw-------.
  • Postinstall happy path — clean install completes and prints [genie] SHA-256 verified: ….

Out of scope

Same scope as the wish:

  • pgserve removal or SQLite migration (explicitly rejected).
  • Replacing pm2 with systemd-user / launchd.
  • Generic dependency upgrade sweep.

Future PRs land G1/G2/G5/G6/G7/G8 per STATUS.md.

🤖 Generated with Claude Code

namastex888 and others added 3 commits May 5, 2026 13:30
Wish: dep-hygiene-and-resilience G3.

Sentinel finding 2026-05-05: ~/.genie/hook-fallback.log was world-readable
with full bash command lines including a token-bearing `gh pr create`. This
log is read by `genie doctor` and operators triaging outages — treat it as a
load-bearing security boundary.

Two fixes, both at the F1 fallback write site (src/hooks/dispatch-client.ts):

1. Mode 0600 enforced. New files created with `appendFileSync(path, data,
   { mode: 0o600 })`. Existing files with looser perms tightened on first
   write after upgrade by `ensureLogPermissions` (idempotent chmod with
   one-line stderr notice).

2. Token-shape redaction at write. New module src/hooks/redaction.ts
   matches gh[ps]_… (30+), sk-… (20+), glpat-… (20+), and a broad 40+
   hex catch for sha-shaped / hex-secret-shaped strings. Substitutes
   [REDACTED:<kind>]. Applied only to record.command field.

Operator opt-out via GENIE_HOOK_REDACTION=off (debugging only — leaves
credentials on disk).

Tests: 12 new in src/hooks/__tests__/redaction.test.ts cover each token
shape, multi-secret strings, no-false-positive plain English, null/undefined
handling, and the env opt-out. All 31 hook tests pass; typecheck clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Wish: dep-hygiene-and-resilience G4.

The same code path that produced an empty bin/bun on 2026-05-05 is the path
a tarball-swap attack would exploit. Pin-or-fail closes both: download then
verify SHA-256 BEFORE writing to disk, BEFORE chmod +x.

Changes:

* package.json — new "binarySha256" block pinning all four tmux 3.6a
  platform tarballs (linux x86_64/arm64, macos x86_64/arm64). Values
  computed live from upstream tmux/tmux-builds release v3.6a.

* scripts/postinstall-tmux.js — getPinnedSha() resolves package.json,
  validates the asset's pinned hash, returns one of:
    pinned     -> compare and abort on mismatch
    missing-key -> hard abort (asset listed nowhere in pin block)
    unpinned    -> soft warn (running standalone outside published pkg)
  SHA verification runs against the in-memory tarball buffer; the
  buffer is only flushed to disk on success.

* scripts/postinstall-hook-binary.js — local-compile path is exempt
  from pinning (we build the binary ourselves) but now logs the entry
  source path and bun executable so operators can verify by other means.

* scripts/postinstall-migrations.js — audit pass; no external fetches,
  no pinning needed. (no diff)

* .github/workflows/binary-sha-drift.yml — on every PR that touches
  package.json or postinstall-*.js, re-fetch each pinned asset and
  compare SHA-256 against the pin. Surfaces drift as a deliberate diff
  comment so version bumps are explicit.

* .genie/wishes/dep-hygiene-and-resilience/binary-sha-bootstrap.md —
  bootstrap procedure for computing initial pins and bumping versions.
  (Lives inside the wish dir because the docs/_internal symlink targets
  an unchecked-out vendor submodule in this worktree; promote later.)

Tamper-test verified locally: matching pin passes, single-byte mutation
produces a different SHA and the script aborts. Typecheck clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Wish: dep-hygiene-and-resilience.

STATUS.md captures: which groups landed (commits), which haven't started
and why, the two outstanding /review MEDIUMs (split G5, add pm2_unavailable
failure path), and the LOWs that can land inline.

Lets the next session pick up without re-reading the full wish narrative.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@gitguardian

gitguardian Bot commented May 5, 2026 •

Copy link
Copy Markdown

⚠️ GitGuardian has uncovered 2 secrets following the scan of your pull request.

Please consider investigating the findings and remediating the incidents. Failure to do so may lead to compromising the associated services or software components.

🔎 Detected hardcoded secrets in your pull request
GitGuardian id GitGuardian status Secret Commit Filename
32492444 Triggered GitHub Personal Access Token 1baaf6e src/hooks/tests/redaction.test.ts View secret
32492445 Triggered GitLab Token 1baaf6e src/hooks/tests/redaction.test.ts View secret
🛠 Guidelines to remediate hardcoded secrets
  1. Understand the implications of revoking this secret by investigating where it is used in your code.
  2. Replace and store your secrets safely. Learn here the best practices.
  3. Revoke and rotate these secrets.
  4. If possible, rewrite git history. Rewriting git history is not a trivial act. You might completely break other contributing developers' workflow and you risk accidentally deleting legitimate data.

To avoid such incidents in the future consider


🦉 GitGuardian detects secrets in your source code to help developers and security teams secure the modern development process. You are seeing this because you or someone else with access to this repository has authorized GitGuardian to scan your pull request.

@coderabbitai

coderabbitai Bot commented May 5, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 10baf0d9-6532-4137-968a-2b84e26857a2

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch wish/dep-hygiene-and-resilience

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces security hardening and dependency hygiene measures, specifically implementing SHA-256 verification for downloaded tmux binaries and enforcing restricted file permissions (0600) with sensitive token redaction for hook fallback logs. Feedback from the reviewer identifies that the post-install scripts do not correctly abort on verification failures because return values are not checked at the entry point, and suggests refining error messages when package metadata is inaccessible.

Comment on lines +130 to +133
console.error(
`[genie] Error: ${asset} SHA-256 mismatch — expected ${pin.sha256}, got ${actual}. Aborting install.`,
);
return false;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

To ensure the installation actually aborts on a SHA-256 mismatch as intended by the 'pin-or-fail' design, you should set a non-zero exit code. Returning false from downloadTmux is insufficient because the script's entry point (line 198) does not check the return value. Using process.exitCode = 1 allows the finally block to perform necessary cleanup before the process terminates.

        console.error(
          "[genie] Error: " + asset + " SHA-256 mismatch — expected " + pin.sha256 + ", got " + actual + ". Aborting install.",
        );
        process.exitCode = 1;
        return false;

Comment on lines +137 to +141
console.error(`[genie] Error: ${asset} has no SHA-256 pin in package.json#binarySha256. Aborting install.`);
console.error(
'[genie] Add the expected SHA-256 to the binarySha256 block. See .genie/wishes/dep-hygiene-and-resilience/binary-sha-bootstrap.md.',
);
return false;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Similar to the mismatch case, a missing mandatory pin should trigger a non-zero exit code to abort the installation. Setting process.exitCode = 1 ensures the failure is propagated to the package manager.

      console.error("[genie] Error: " + asset + " has no SHA-256 pin in package.json#binarySha256. Aborting install.");
      console.error(
        "[genie] Add the expected SHA-256 to the binarySha256 block. See .genie/wishes/dep-hygiene-and-resilience/binary-sha-bootstrap.md.",
      );
      process.exitCode = 1;
      return false;

Comment on lines +146 to +148
`[genie] Warning: no binarySha256 block found in package.json — skipping SHA verification for ${asset}.`,
);
console.error('[genie] This path is for local-dev only; published installs must pin.');

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The warning message assumes the binarySha256 block is missing, but getPinnedSha can also return unpinned if reading package.json fails (e.g., due to a file system error). It would be more accurate to include the specific reason if provided.

      console.error(
        "[genie] Warning: skipping SHA verification for " + asset + (pin.reason ? ": " + pin.reason : " (no binarySha256 block found in package.json)") + ".",
      );

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5658299f19

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +129 to +133
if (actual.toLowerCase() !== pin.sha256) {
console.error(
`[genie] Error: ${asset} SHA-256 mismatch — expected ${pin.sha256}, got ${actual}. Aborting install.`,
);
return false;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Exit non-zero when SHA verification fails

The new SHA check paths return false on mismatch/missing pin, but this does not actually abort installation because the script’s top-level call only awaits ensureTmux() and never converts that false into a non-zero exit code. In practice, a tampered tarball still lets postinstall succeed (exit 0), so the advertised pin-or-fail guarantee is not enforced for users running bun add -g/postinstall.

Useful? React with 👍 / 👎.

Comment thread src/hooks/redaction.ts
{ kind: 'glpat', re: /glpat-[A-Za-z0-9_-]{20,}/g },
// Broad catch for sha-shaped or hex-secret-shaped strings (40+ hex chars).
// Word-boundary keeps this from clipping inside larger tokens already redacted.
{ kind: 'hex', re: /\b[a-f0-9]{40,}\b/g },

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Redact uppercase hex secrets in fallback logs

The generic hex redaction pattern is lowercase-only, so any 40+ char token containing uppercase A-F is logged unredacted. This leaves a credential-leak path in the fallback log for environments/tools that emit uppercase hex identifiers, despite the function claiming to redact generic hex secret shapes.

Useful? React with 👍 / 👎.

GitGuardian on PR #1664 flagged two synthetic test inputs as live secrets
(redaction.test.ts:23 ghp_ token, :42 glpat- token). They're test fixtures
designed to verify the redaction regex catches the right shapes — but the
contiguous prefix literal in source matches the scanner's detector.

Split the prefix at any position so the literal `ghp_`, `ghs_`, `sk-`,
and `glpat-` never appears as a contiguous source-level string. Runtime
concatenation produces the identical input the regex sees, so test logic
is unchanged. Verified: 12/12 pass, biome clean, grep finds zero
contiguous token-shape literals in the source file.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@namastex888
namastex888 merged commit 94e05ac into dev May 5, 2026
17 checks passed
@automagik-genie
automagik-genie deleted the wish/dep-hygiene-and-resilience branch September 25, 2026 04:51
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