Your First validate-packet.sh - Bash and PowerShell Guards for Q3 2026 Partner Packets
You ran sha256sum once. You eyeball-compared MANIFEST.json. You uploaded anyway. Q3 2026 partner intake returned yellow on checksum parity while your Discord still says “green CI.”
Scripts do not replace judgment—they remove the steps you skip when tired. This Programming & Technical tutorial builds validate-packet.sh (Bash) and a PowerShell twin with the same gates: receipt present, manifest parses, sums match manifest paths, optional cold diff empty. Pair with BUILD_RECEIPT evening, SHA256 cold drill, and the validator listicle.
Who this is for and what you get
| Audience | You will be able to… |
|---|---|
| Solo dev with Git Bash or WSL | Run one command before every zip upload |
| Windows-primary team | Use Validate-Packet.ps1 with identical exit codes |
| Lead preparing mock audit | Demo dimension 2 checks in the room |
Time: ~90 minutes first implementation; under ten seconds per run after that.
Prerequisites: jq, sha256sum or Get-FileHash, release-evidence/ layout from folder taxonomy.
Why this matters now (May 2026)
- Intake compression — You get 72-hour recovery windows, not six-week polish buffers.
- Cold-hash challenge day 7 expects a script exit 0.
- Hash mismatch case study — Recovery started when teams had no script—only hope.
- Weekly patch opinion — Evidence cycles need automation, not hero uploads.
Direct answer: Commit release-evidence/scripts/validate-packet.sh; require exit 0 before portal upload and log output to validation/.
Script contract (both platforms)
| Exit code | Meaning |
|---|---|
| 0 | All gates pass |
| 1 | Missing file or bad arguments |
| 2 | JSON parse failure |
| 3 | Manifest vs sums mismatch |
| 4 | Receipt build_id mismatch |
| 5 | Non-empty diff (cold sums) |
Document codes in release-evidence/README.md. CI and humans read the same legend.
Folder assumptions
release-evidence/
BUILD_RECEIPT.json # or path passed as flag
SHA256SUMS.txt
MANIFEST.json
files_to_hash.txt
01-build/game/ # hash cwd
scripts/
validate-packet.sh
Validate-Packet.ps1
validation/ # logs written here
Adjust paths via flags—defaults match partner ZIP naming examples.
Gate 1 — Files exist
Bash:
need() { test -f "$1" || { echo "missing $1"; exit 1; }; }
need "$ROOT/BUILD_RECEIPT.json"
need "$ROOT/SHA256SUMS.txt"
need "$ROOT/MANIFEST.json"
need "$ROOT/files_to_hash.txt"
PowerShell:
function Need-File($p) {
if (-not (Test-Path $p)) { Write-Error "missing $p"; exit 1 }
}
Fail fast before jq runs on empty paths.
Gate 2 — Receipt build_id
EXPECTED_BUILD_ID="${1:?pass expected build_id}"
ACTUAL=$(jq -r '.build_id' "$ROOT/BUILD_RECEIPT.json")
test "$ACTUAL" = "$EXPECTED_BUILD_ID" || { echo "build_id mismatch"; exit 4; }
Pass build_id as first argument matching planned zip filename—stops README/receipt/zip drift.
Gate 3 — Manifest vs SHA256SUMS (detailed)
Partners treat manifest and sidecar as one truth surface. Your script should verify:
- Every
files[].pathinMANIFEST.jsonhas a sums line. - Every
files[].sha256matches that line’s hex. - No required gameplay file is missing from manifest (optional fourth check against
files_to_hash.txt).
Bash loop (readable):
while IFS= read -r line; do
path=$(jq -r --arg p "$line" '.files[] | select(.path==$p) | .path' "$ROOT/MANIFEST.json")
hash=$(jq -r --arg p "$line" '.files[] | select(.path==$p) | .sha256' "$ROOT/MANIFEST.json")
if [[ -z "$path" ]]; then echo "manifest missing $line"; exit 3; fi
grep -q "^${hash} ${path}$" "$ROOT/SHA256SUMS.txt" || { echo "sums mismatch $path"; exit 3; }
done < "$ROOT/files_to_hash.txt"
Why read files_to_hash.txt? It is the frozen list from cold-hash day 1—manifest cannot omit files you committed to hash.
CRLF pitfall: If sums were generated on Windows with CRLF line endings in the file list, normalize lists before hashing—see hash case study.
Gate 4 — Regenerate sums and diff
GAME_DIR="$ROOT/01-build/game"
(
cd "$GAME_DIR"
sha256sum $(cat "$ROOT/files_to_hash.txt") > "$ROOT/SHA256SUMS.recomputed.txt"
)
diff -u "$ROOT/SHA256SUMS.txt" "$ROOT/SHA256SUMS.recomputed.txt" || exit 5
Empty diff means export machine matches list. Run again on cold laptop before upload—not only on dev PC.
Gate 5 — Log success
LOG="$ROOT/validation/$(date -u +%Y-%m-%dT%H%M%SZ)_validate_packet_pass.log"
{
echo "validate-packet pass"
echo "build_id=$EXPECTED_BUILD_ID"
echo "host=$(hostname)"
} > "$LOG"
exit 0
Friday Block 5 can grep latest *_pass.log.
Full Bash skeleton (copy-paste starter)
#!/usr/bin/env bash
set -euo pipefail
ROOT="${2:-.}"
EXPECTED_BUILD_ID="${1:?usage: validate-packet.sh BUILD_ID [ROOT]}"
need() { test -f "$1" || { echo "missing $1"; exit 1; }; }
need "$ROOT/BUILD_RECEIPT.json"
need "$ROOT/SHA256SUMS.txt"
need "$ROOT/MANIFEST.json"
need "$ROOT/files_to_hash.txt"
ACTUAL=$(jq -r '.build_id' "$ROOT/BUILD_RECEIPT.json")
[[ "$ACTUAL" == "$EXPECTED_BUILD_ID" ]] || { echo "build_id mismatch"; exit 4; }
while read -r path hash; do
grep -q "^${hash} ${path}$" "$ROOT/SHA256SUMS.txt" || { echo "manifest/sums mismatch $path"; exit 3; }
done < <(jq -r '.files[] | "\(.path) \(.sha256)"' "$ROOT/MANIFEST.json")
GAME_DIR="$ROOT/01-build/game"
( cd "$GAME_DIR"; sha256sum $(cat "$ROOT/files_to_hash.txt") > "$ROOT/SHA256SUMS.recomputed.txt" )
diff -u "$ROOT/SHA256SUMS.txt" "$ROOT/SHA256SUMS.recomputed.txt"
LOG_DIR="$ROOT/validation"; mkdir -p "$LOG_DIR"
echo "pass build_id=$EXPECTED_BUILD_ID utc=$(date -u +%Y-%m-%dT%H:%M:%SZ)" > "$LOG_DIR/latest_validate_packet_pass.log"
echo "validate-packet: OK"
Run shellcheck validate-packet.sh per listicle tool 15.
PowerShell skeleton (exit code parity)
param(
[Parameter(Mandatory=$true)][string]$BuildId,
[string]$Root = "."
)
$ErrorActionPreference = "Stop"
function Need($p) { if (-not (Test-Path $p)) { exit 1 } }
Need (Join-Path $Root "BUILD_RECEIPT.json")
Need (Join-Path $Root "SHA256SUMS.txt")
Need (Join-Path $Root "MANIFEST.json")
Need (Join-Path $Root "files_to_hash.txt")
$receipt = Get-Content (Join-Path $Root "BUILD_RECEIPT.json") -Raw | ConvertFrom-Json
if ($receipt.build_id -ne $BuildId) { exit 4 }
$manifest = Get-Content (Join-Path $Root "MANIFEST.json") -Raw | ConvertFrom-Json
foreach ($f in $manifest.files) {
$line = Select-String -Path (Join-Path $Root "SHA256SUMS.txt") -Pattern "^$($f.sha256) $($f.path)$"
if (-not $line) { exit 3 }
}
$game = Join-Path $Root "01-build\game"
$list = Get-Content (Join-Path $Root "files_to_hash.txt")
$out = foreach ($rel in $list) {
$h = (Get-FileHash -Path (Join-Path $game $rel) -Algorithm SHA256).Hash.ToLower()
"$h $rel"
}
$recomputed = Join-Path $Root "SHA256SUMS.recomputed.txt"
$out | Set-Content $recomputed -Encoding utf8NoBOM
$diff = Compare-Object (Get-Content (Join-Path $Root "SHA256SUMS.txt")) (Get-Content $recomputed)
if ($diff) { exit 5 }
$logDir = Join-Path $Root "validation"
New-Item -ItemType Directory -Force -Path $logDir | Out-Null
"pass build_id=$BuildId utc=$((Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ'))" |
Set-Content (Join-Path $logDir "latest_validate_packet_pass.log")
Write-Host "validate-packet: OK"
exit 0
Save as Validate-Packet.ps1. Invoke:
.\release-evidence\scripts\Validate-Packet.ps1 -BuildId "northlake_game_steam_abc123_cert_20260518" -Root ".\release-evidence"
Ninety-minute implementation sprint
| Block | Minutes | Task |
|---|---|---|
| A | 15 | Install jq; verify sha256sum |
| B | 25 | Paste Bash skeleton; fix paths |
| C | 20 | Port to PowerShell; match exit codes |
| D | 15 | shellcheck + test on sample packet |
| E | 15 | Wire pre-upload checklist in README |
Stop when both platforms exit 0 on frozen build_id.
Testing the script locally (before real packet)
Create a toy packet in release-evidence/_test/:
- Copy minimal
MANIFEST.jsonwith one fake file entry. - Place one small binary in
01-build/game/. - Generate real sums from
files_to_hash.txt. - Run script expecting pass.
- Change one manifest hash digit—expect exit 3.
- Change receipt
build_id—expect exit 4.
Toy tests take twenty minutes and prevent shipping a script that always exits 0 because paths are wrong.
Flags and configuration (production hardening)
Extend skeleton with optional flags:
| Flag | Purpose |
|---|---|
--root PATH |
Evidence root |
--build-id ID |
Expected id |
--skip-diff |
Manifest-only quick check |
--cold |
Require SHA256SUMS.recomputed.txt from env var path |
--log-dir |
Override validation output |
Document flags in release-evidence/scripts/README.md. Micro-studios should not need twelve flags—defaults first, flags when two storefront zips share scripts.
Two-storefront invocation
Under two-storefront rule:
./validate-packet.sh northlake_game_steam_abc_cert_20260518 ./release-evidence/steam
./validate-packet.sh northlake_game_epic_abc_cert_20260518 ./release-evidence/epic
Separate roots or separate build_id args—never one script run pretending two channels share hashes.
Receipt extensions the script can assert
Optional jq checks when extensions exists:
jq -e '.extensions.validator_script_commit | length > 0' "$ROOT/BUILD_RECEIPT.json" >/dev/null
During RC freeze, pin validator commit in receipt—script can git rev-parse compare when run inside repo.
Asia-EU handoff integration
Handoff notes should cite:
validate_packet_log: validation/latest_validate_packet_pass.log
Overnight exporter runs script before dropping zip in 00-handoff/outbox/. Morning reviewer uploads only if log timestamp is after export.
AI annex files
When 04_ai/disclosure_matrix.csv ships, append paths to files_to_hash.txt before gate 4. Script does not know AI semantics—it knows paths. Run AI disclosure challenge before adding annex rows or gate 3 fails legitimately.
Debugging exit codes (quick reference)
| Exit | First look |
|---|---|
| 1 | Path typo; file not exported |
| 2 | Invalid JSON; trailing comma |
| 3 | Manifest path or hash typo |
| 4 | Receipt not updated after export |
| 5 | Bytes changed since sums written |
Fix root cause—do not bump exit code meanings mid-quarter.
WSL vs native Windows
Windows teams often use Git Bash for sha256sum parity and PowerShell for daily work. Pick one cold-machine toolchain and document in portal_notes.md. Mixing tools without utf8NoBOM discipline recreates case study failures.
Versioning validate-packet.sh
Tag script in receipt:
"extensions": {
"validate_packet_version": "1.0.0"
}
Bump minor version when adding flags; bump major when exit codes change. Publisher diligence reviewers notice silent semantic drift.
Operator checklist (print)
[ ] build_id argument matches zip filename
[ ] jq --version recorded in log
[ ] validate-packet exit 0 on export machine
[ ] validate-packet exit 0 on cold machine
[ ] latest_validate_packet_pass.log committed or archived
[ ] upload_log.csv row pending until after portal click
CI integration (optional, advisory)
GitHub Actions example—advisory until receipts stable:
- name: Validate partner packet
run: ./release-evidence/scripts/validate-packet.sh "${{ vars.BUILD_ID }}" ./release-evidence
Do not block merges on day one—block upload manually until logs look boring.
Pre-commit hook (narrow)
From listicle tool 16:
# .git/hooks/pre-commit snippet
if git diff --cached --name-only | grep -q '^release-evidence/01-build/'; then
./release-evidence/scripts/validate-packet.sh "$(jq -r .build_id release-evidence/BUILD_RECEIPT.json)" release-evidence || exit 1
fi
Only when receipt build_id is current—avoid blocking unrelated commits.
Common mistakes
- Running from wrong cwd —
01-build/gamepaths break. - Uppercase SHA256 on Windows — lowercase before compare.
- Skipping cold machine run — script passes on dev, fails partner laptop.
- No build_id argument — zip filename drifts.
- jq missing on cold PC — install before upload trip.
- Trailing commas in JSON — gate 2 fails; fix manifest.
- Empty files_to_hash.txt — passes trivially; useless.
Pairing with cold-hash challenge day 7
Day 7 requires script exit 0 on cold hardware. This tutorial is the script content for that gate.
Pairing with resubmission
After 72-hour recovery, bump build_id, regen files, run script, attach log to RESUBMISSION_NOTE.md reference line.
Security notes
- Script must not echo secrets or API keys from receipt JSON.
- Do not curl | bash partner templates—read scripts you commit.
- Log files may contain hostnames—fine for internal evidence.
Key takeaways
validate-packet.shencodes gates partners simulate—receipt, manifest, sums, diff.- Match exit codes across Bash and PowerShell for one runbook.
- Pass
build_idas argument aligned with zip name. - Write logs under
validation/for mock audit and diligence. - Run on cold machine before upload, not only CI.
shellcheckbefore trusting Bash; utf8NoBOM on Windows sums.- Script supports evidence cycles—not weekly patch theater.
FAQ
Must we use both Bash and PowerShell?
No—pick export-machine platform; cold machine needs same tooling or WSL.
Can Python replace jq?
Yes—keep exit code contract identical.
Does this upload to portal?
No—local gate only. Upload remains human click with log proof.
What if manifest has fifty files?
Script time stays seconds—optimize only if lists exceed thousands.
How does this relate to validate-packet in listicle?
Listicle names tools; this tutorial ships the script body.
Should the script run inside the zip?
No—run against release-evidence/ tree before zipping. Optionally run again on unpacked zip on cold machine per cold drill.
Can we call this from Unity or Godot?
Yes—Process.Start invoking Bash or PowerShell with captured exit code—keep game engine out of hash logic; call script as external tool.
Extended manifest jq recipes
Assert primary_executable exists:
jq -e '.primary_executable | length > 0' "$ROOT/MANIFEST.json"
Assert hash_sidecar filename:
jq -r '.hash_sidecar' "$ROOT/MANIFEST.json" | grep -qx 'SHA256SUMS.txt'
Count files matches list:
test "$(jq '.files | length' "$ROOT/MANIFEST.json")" -eq "$(wc -l < "$ROOT/files_to_hash.txt")"
Small assertions catch README lies before partners do.
Performance and scale
Indie packets rarely exceed hundreds of files. Script runtime dominated by disk IO, not jq. If lists exceed ~2k files, switch grep loop to awk associative arrays—optimization only when measured slow.
Maintenance cadence
| Event | Script action |
|---|---|
| New file type in zip | Update files_to_hash.txt |
| Path convention change | Update GAME_DIR variable |
| New exit code | Major version bump + README |
| RC freeze week | Pin commit in receipt |
Align with operating review Block 3.
Honest limits
Script does not:
- Replace legal review
- Validate gameplay fun
- Upload to portal
- Fix bad art or store copy
It validates artifact graph consistency—the Lane A foundation in intake compression analysis.
Gate-by-gate failure messages (user-facing)
Write stderr messages partners never see but your team will:
| Gate | Message example |
|---|---|
| 1 | ERROR: missing MANIFEST.json at $ROOT |
| 3 | ERROR: sums line missing for path game/foo.dll |
| 4 | ERROR: receipt build_id northlake_a != arg northlake_b |
| 5 | ERROR: SHA256SUMS diff non-empty; see SHA256SUMS.recomputed.txt |
Clear messages shorten 72-hour recovery debates.
Integrating with 7z test
After script pass, run:
7z t partner_packet.zip || exit 1
Log zip test in same validation folder. Corrupt zip passes hash on source tree but fails partner open—cheap addition before drive to upload café.
Upload_log hook (pseudo-code)
After exit 0:
echo "$(date -u +%Y-%m-%dT%H:%M:%SZ),$EXPECTED_BUILD_ID,partner_cert,${ZIP_NAME},PENDING,submitted,validate-packet exit 0" \
>> "$ROOT/../05-operations/upload_log.csv"
Adjust path to your taxonomy—BUILD_RECEIPT tutorial defines CSV columns.
Mock audit demo script
In mock audit dimension 2, run:
./validate-packet.sh "$BUILD_ID" ./release-evidence; echo exit=$?
cat ./release-evidence/validation/latest_validate_packet_pass.log
Tabletop participants should see exit 0 and fresh UTC timestamp—evidence, not slides.
Symbols zip variant
When shipping _symbols.zip, duplicate script with GAME_DIR pointing at symbols root or pass --game-dir flag. Never hash gameplay and symbols in one manifest without documenting roles—see SHA256 drill symbols section.
Engine export hooks
Unity / Godot / Unreal should not embed hash logic in builds. Standard pattern:
- CI or local export job writes
01-build/. - Release owner runs validate script.
- Only then copy into partner zip staging.
Keeps engine projects free of partner-specific path assumptions.
Script anti-patterns
- Hard-coded
C:\paths - Ignoring exit codes in CI
|| trueafter diff- Skipping shellcheck to “save time”
- One manifest for two channel zips
- Running script only after yellow flag
Each pattern appeared in composite recovery stories on this blog—avoid becoming the next composite.
Future: jsonschema gate
When ready, add Python gate reading BUILD_RECEIPT.schema.json—exit 2 on schema fail. Bash+jq remains the 2026 floor; schema is ceiling for mature teams.
Team onboarding blurb
Paste in repo CONTRIBUTING:
Before any partner upload,
validate-packet.shmust exit 0 on export and cold machines. Attachvalidation/latest_validate_packet_pass.logto handoff notes. No exceptions during Q3 cert prep.
Ten lines beat forty-minute oral tradition.
Read order for cert cluster
- Folder taxonomy
- BUILD_RECEIPT evening
- SHA256 drill
- This script tutorial
- Partner ZIP naming
- Cold-hash challenge
Script is step four—after you know what files mean, before you name the zip.
Line-by-line walkthrough of the Bash skeleton
set -euo pipefail— stop on first failed command; essential for diff gate.EXPECTED_BUILD_IDargument — ties CLI to zip filename convention.needchecks — cheap failures before jq parses garbage.build_idjq extract — catches receipt typos.- Manifest loop — enforces sums lines match structured JSON.
- Subshell
cd— guarantees hash cwd matches zip interior layout. diff -u— empty output is the pass signal; non-empty is exit 5.- Log file — proves when and where pass happened for audits.
Remove any step and you recreate a class of hash mismatch failures.
Dry-run mode (recommended flag)
Add --dry-run that prints gates without exit 5 recompute—useful when iterating paths:
echo "DRY: would hash $(wc -l < "$ROOT/files_to_hash.txt") files"
echo "DRY: would diff SHA256SUMS.txt"
Dry-run helps beginners learn layout without waiting on large binaries.
Q3 template cross-reference
Q3 submission templates supply folder names; this script supplies pass/fail. Install both in the same sprint—folders without gates are decoration.
Success checks for this tutorial
You finished when:
- Bash and PowerShell both exit 0 on same frozen packet
- Cold machine log exists with different hostname
- You can force exit 3 by editing one manifest hash and see stderr message
- Pre-upload checklist in repo mentions script
- Mock audit dimension 2 marked pass with log path cited
Troubleshooting workshop (30 minutes)
Pair with a teammate: one person breaks manifest hash, one runs script, swap. Breaking and fixing in thirty minutes teaches more than reading three thousand words alone. Common break types: uppercase hex, wrong path prefix, stale receipt commit, missing annex file in list. Each maps to exit codes you now document in release-evidence/README.md. Schedule the workshop before your first real portal upload, not after the first yellow email.
Conclusion
validate-packet.sh is how micro-studios make SHA256 cold validation repeatable on upload night—not a one-time hero effort. May 2026 is the right week to commit the script beside BUILD_RECEIPT templates and refuse portal clicks until exit 0 prints on cold metal.
Ninety minutes today buys fewer 72-hour weekends later. Run the skeleton, fix your paths, log the pass, and upload with a receipt you can defend.
If the script feels pedantic, remember: partners run pedantic checks on cold laptops. Your job is to run them first on hardware you control, log the result, and treat exit 0 as the real definition of “ready to upload”—not the moment you feel done. Save the log beside the ZIP every time. No exceptions on any release branches.