Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions .github/workflows/security-scan.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
name: Security scan

on:
pull_request:
push:
branches:
- main
workflow_dispatch:

permissions:
contents: read
pull-requests: read

env:
GITLEAKS_VERSION: v8.30.1

jobs:
gitleaks:
name: Gitleaks
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6

- name: Set up Go
uses: actions/setup-go@v6
with:
go-version: "1.24.11"
cache: false

- name: Scan current tree for secrets
run: go run github.com/zricethezav/gitleaks/v8@${GITLEAKS_VERSION} dir --redact --config .gitleaks.toml .

stack-sensitive-content:
name: Stack sensitive content
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6

- name: Scan stack-specific sensitive content
run: scripts/security/scan-sensitive-content.sh
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
config/settings.local.json
node_modules/
.DS_Store
gitleaks-report*.json
gitleaks-findings*.json
trufflehog-report*.json
23 changes: 23 additions & 0 deletions .gitleaks.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
title = "Stack secret scanning"
minVersion = "v8.25.0"

[extend]
useDefault = true

[[rules]]
id = "stack-local-maroun-path"
description = "Local Maroun machine paths should not be published in the stack repo."
regex = '''/Users/maroun/[A-Za-z0-9._~/%+-]+'''
keywords = ["/Users/maroun"]

[[rules]]
id = "stack-codex-private-artifact"
description = "Codex session, memory, and rollout artifacts can contain private context."
regex = '''(?i)(?:\.codex/(?:sessions|archived_sessions|memories)|rollout-[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9-]+-[0-9a-f-]+\.jsonl)'''
keywords = [".codex", "rollout-"]

[[rules]]
id = "stack-config-secret-assignment"
description = "Agent or tool config appears to contain a concrete secret value."
regex = '''(?i)(?:api[_-]?key|access[_-]?token|refresh[_-]?token|client[_-]?secret|password|private[_-]?key)\s*[:=]\s*['"]?[A-Za-z0-9_./+=-]{16,}'''
keywords = ["api", "key", "token", "secret", "password"]
18 changes: 18 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
default_install_hook_types: [pre-commit, pre-push]

repos:
- repo: https://github.com/gitleaks/gitleaks
rev: v8.30.1
hooks:
- id: gitleaks
name: Gitleaks staged secret scan
stages: [pre-commit]

- repo: local
hooks:
- id: stack-sensitive-content
name: Stack sensitive content scan
entry: scripts/security/scan-sensitive-content.sh
language: script
pass_filenames: false
stages: [pre-commit, pre-push]
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,14 @@ claude plugin marketplace add tmchow/illo-skill
claude plugin install illo@illo-skill
```

## Security Guardrails

Pull requests run a `Security scan` workflow before merge. The workflow combines
Gitleaks with a stack-specific sensitive content scanner for local paths, private
agent artifacts, credential-looking config values, and household or finance-lane
references. See `docs/security/leak-prevention.md` for local hook setup and leak
remediation guidance.

---

## Philosophy
Expand Down
57 changes: 57 additions & 0 deletions docs/security/leak-prevention.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Leak Prevention

This repo is designed to be shared, so every pull request should pass two layers
of secret and sensitive-content checks before merge.

## Required PR Gate

The `Security scan` GitHub Actions workflow runs on pull requests and on pushes
to `main`.

- `Gitleaks` catches provider tokens, private keys, connection strings, and other
standard secret patterns in the current tree.
- `Stack sensitive content` catches repo-specific risks such as local machine
paths, Codex session artifacts, credential-like agent config fields, and
household or finance-lane references.

Scanner output intentionally reports only file, line, and rule name. Do not paste
the matched secret value into chat, issues, pull request comments, or commit
messages while fixing a failure.

## Optional Local Hook

Install local hooks when working on this repo:

```bash
cd ~/Projects/stack
pre-commit install
```

Then run the checks manually before opening a PR:

```bash
pre-commit run --all-files
scripts/security/scan-sensitive-content.sh
go run github.com/zricethezav/gitleaks/v8@v8.30.1 dir --redact --config .gitleaks.toml .
```

The local hook is a convenience layer. GitHub Actions remains the enforced gate.

For a one-time audit of older commits, run Gitleaks in git-history mode:

```bash
go run github.com/zricethezav/gitleaks/v8@v8.30.1 git --redact --config .gitleaks.toml .
```

Treat real credentials in history as leaked and rotate them. Historical local
path findings should be cleaned from the current tree, but they do not require
credential rotation.

## If A Leak Is Found

1. Remove the sensitive value from the branch.
2. Rotate or revoke the credential if a real token, password, private key, or
session artifact was committed or pushed.
3. Keep remediation notes generic: name the provider and file path, not the
secret value.
4. Re-run the scanners before requesting review.
137 changes: 137 additions & 0 deletions scripts/security/scan-sensitive-content.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
#!/usr/bin/env bash
set -euo pipefail

repo_root="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
cd "$repo_root"

failures=0

ignored_path() {
case "$1" in
.git/*|node_modules/*|scripts/security/scan-sensitive-content.sh|.gitleaks.toml)
return 0
;;
*)
return 1
;;
esac
}

binary_or_unreadable() {
[[ ! -f "$1" ]] && return 0
grep -Iq . "$1" 2>/dev/null
}

report() {
local file="$1"
local line="$2"
local rule="$3"

printf '%s:%s: sensitive-content: %s\n' "$file" "$line" "$rule" >&2
failures=$((failures + 1))
}

scan_path_name() {
local file="$1"

case "$file" in
*.pem|*.p12|*.pfx|*.key|*.asc|*.kdbx|*.mobileconfig)
report "$file" 0 "private key, certificate, or credential-like file extension"
;;
.env|.env.*|*/.env|*/.env.*|*.env)
case "$file" in
*.example|*.sample|*.template)
;;
*)
report "$file" 0 "environment file should not be committed"
;;
esac
;;
*/settings.local.json|settings.local.json|*.local.json|*.secrets.*|*secret*.json|*credentials*.json)
report "$file" 0 "local settings or credential-like file name"
;;
esac
}

scan_line() {
local file="$1"
local findings
local count

findings="$(
awk -v file="$file" '
function placeholder(line) {
line = tolower(line)
return line ~ /\$\{\{[^}]*secrets\.[^}]*}}/ ||
line ~ /<your_|<example|example_|changeme|replace_me|not-a-real|redacted|placeholder/
}

function finding(rule) {
print file ":" NR ": sensitive-content: " rule
}

{
lower = tolower($0)

if ($0 ~ /-----BEGIN[[:space:]][A-Z0-9[:space:]]*PRIVATE[[:space:]]KEY-----/) {
finding("private key material")
}

if ($0 ~ /https?:\/\/[^[:space:]\/:@]+:[^[:space:]@]+@/) {
finding("URL contains embedded credentials")
}

if ($0 ~ /\/Users\/maroun\//) {
finding("local Maroun machine path")
}

if ($0 ~ /\.codex\/(sessions|archived_sessions|memories)/ ||
$0 ~ /rollout-[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9-]+-[0-9a-f-]+\.jsonl/) {
finding("private Codex session or memory artifact")
}

if (lower ~ /(finance\/amazon|amazon-collette|account_snapshots|monarch|copilot[[:space:]_-]*account)/) {
finding("personal finance or household account lane")
}

if (!placeholder($0) &&
$0 ~ /(sk-ant-api03-[A-Za-z0-9_-]{20,}|sk-proj-[A-Za-z0-9_-]{20,}|sk-[A-Za-z0-9_-]{32,}|gh[pousr]_[A-Za-z0-9_]{32,}|github_pat_[A-Za-z0-9_]{32,}|AKIA[0-9A-Z]{16}|AIza[0-9A-Za-z_-]{35}|xox[baprs]-[A-Za-z0-9-]{20,})/) {
finding("provider token pattern")
}

if (!placeholder($0) &&
lower ~ /(api[_-]?key|access[_-]?token|refresh[_-]?token|client[_-]?secret|password|private[_-]?key)[[:space:]]*[:=][[:space:]]*["'\''"]?[a-z0-9_.\/+=-]{16,}/) {

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 Allow quoted JSON secret keys in assignment scan

The assignment regex requires : or = immediately after the key name, so a normal JSON config line like "api_key": "..." has a quote between api_key and : and is not flagged. In a PR adding generic tokens/passwords to JSON files that do not match a provider-specific pattern, both the stack scanner and the matching Gitleaks rule miss the secret-looking assignment; allow an optional closing quote before the separator.

Useful? React with 👍 / 👎.

finding("secret-looking config assignment")
}
}
' "$file"
)"

if [[ -n "$findings" ]]; then
printf '%s\n' "$findings" >&2
count="$(printf '%s\n' "$findings" | wc -l | tr -d ' ')"
failures=$((failures + count))
fi
}

scan_file() {
local file="$1"

ignored_path "$file" && return 0
binary_or_unreadable "$file" || return 0

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 Move filename checks before binary skips

Because binary_or_unreadable is evaluated before scan_path_name, any binary credential file that the filename rules explicitly list (.p12, .pfx, .kdbx, etc.) returns here before its extension is reported. In a PR that adds a real binary cert/key store, this job would pass even though the filename rule intends to block it; run scan_path_name before the text/binary guard and only skip scan_line for binary files.

Useful? React with 👍 / 👎.


scan_path_name "$file"
scan_line "$file"
}

while IFS= read -r -d '' file; do
scan_file "$file"
done < <(git ls-files -co --exclude-standard -z)

if (( failures > 0 )); then
printf '\nStack sensitive content scan failed with %s finding(s).\n' "$failures" >&2
printf 'Review the flagged lines without pasting secret values into chat or PR comments.\n' >&2
exit 1
fi

printf 'Stack sensitive content scan passed.\n'
32 changes: 16 additions & 16 deletions skills/stack-refresh-pr-mode/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: stack-refresh-pr-mode
description: "Run Maroun's review-safe Claude/Codex skill stack refresh from /Users/maroun/Codex. Use when asked to update, refresh, verify, or diagnose the local skill stack, the update-last30days-skill automation, gstack/compound-engineering pins, Impeccable, last30days, illo, transitions-dev-motion, or Stack repo PR-mode behavior without pushing directly to main."
description: "Run Maroun's review-safe Claude/Codex skill stack refresh from ~/Codex. Use when asked to update, refresh, verify, or diagnose the local skill stack, the update-last30days-skill automation, gstack/compound-engineering pins, Impeccable, last30days, illo, transitions-dev-motion, or Stack repo PR-mode behavior without pushing directly to main."
---

# Stack Refresh PR Mode
Expand All @@ -9,19 +9,19 @@ Use this skill for the recurring skill-stack update loop. The goal is a verified

## Source Order

1. Read `/Users/maroun/Codex/README.md`.
2. Read `/Users/maroun/Codex/upstreams.lock.json`.
3. Read `/Users/maroun/.codex/automations/update-last30days-skill/memory.md` if it exists.
4. Check `/Users/maroun/Projects/stack` branch and status before running anything that might sync or push.
1. Read `~/Codex/README.md`.
2. Read `~/Codex/upstreams.lock.json`.
3. Read `~/.codex/automations/update-last30days-skill/memory.md` if it exists.
4. Check `~/Projects/stack` branch and status before running anything that might sync or push.

Do not inspect, print, or modify `/Users/maroun/.config/last30days/.env` or `/Users/maroun/.config/illo/config.yaml`. It is OK to report whether protected config files exist when the run asks for that.
Do not inspect, print, or modify `~/.config/last30days/.env` or `~/.config/illo/config.yaml`. It is OK to report whether protected config files exist when the run asks for that.

## Refresh Command

Run from `/Users/maroun/Codex`:
Run from `~/Codex`:

```bash
SYNC_STACK_REPO_TO_GITHUB=pr /Users/maroun/.codex/scripts/update-last30days-skill.sh
SYNC_STACK_REPO_TO_GITHUB=pr ~/.codex/scripts/update-last30days-skill.sh
```

Use the exact command unless the user explicitly changes it. Do not replace it with manual installs, ad hoc copies, or direct runtime edits.
Expand All @@ -35,13 +35,13 @@ When validating this skill or auditing readiness, do not run the refresh command
Verify every requested installed skill path. The usual set is:

```text
/Users/maroun/.codex/skills/last30days/SKILL.md
/Users/maroun/.codex/skills/gstack/SKILL.md
/Users/maroun/.codex/skills/ce-setup/SKILL.md
/Users/maroun/.codex/skills/teach-impeccable/SKILL.md
/Users/maroun/.codex/skills/impeccable/SKILL.md
/Users/maroun/.codex/skills/transitions-dev-motion/SKILL.md
/Users/maroun/.codex/skills/illo/SKILL.md
~/.codex/skills/last30days/SKILL.md
~/.codex/skills/gstack/SKILL.md
~/.codex/skills/ce-setup/SKILL.md
~/.codex/skills/teach-impeccable/SKILL.md
~/.codex/skills/impeccable/SKILL.md
~/.codex/skills/transitions-dev-motion/SKILL.md
~/.codex/skills/illo/SKILL.md
```

Also report:
Expand All @@ -58,7 +58,7 @@ Stop and ask before:
- changing protected config or secrets;
- widening the task into external account, browser profile, credential, or production-state changes.

If `/Users/maroun/Projects/stack` is not on `main`, keep PR-mode behavior and report the branch gate instead of forcing a checkout.
If `~/Projects/stack` is not on `main`, keep PR-mode behavior and report the branch gate instead of forcing a checkout.

## Closeout

Expand Down