Application repos get linters, secret scanners, and CI. Shell script repos often get none of it, even when those scripts run as root on every Mac in the fleet. That gap is what I built a workshop around for the Mac Admins Conference in July 2026, and the materials are now a repo anyone can work through on their own.
This is the written version of the idea, plus the parts I’d tell you even if you never attend a workshop.
The Premise
Everything is built by hand first. The point of the session isn’t to hand over a framework; it’s to make sure you understand each piece before anything automates it. You finish with a working repo containing:
| Piece | What it does |
|---|---|
| Versioned script | A baseline structure with a proper header |
| Gitleaks | Blocks commits that contain secrets |
| ShellCheck | Catches quoting mistakes and shell errors before they ship |
| bump-version | Handles semantic versioning in the script header and CHANGELOG |
| GitHub Actions | Enforces Gitleaks and ShellCheck on every pull request |
The last segment shows Shikomi, which generates all of the above in one command. The ordering is deliberate. You appreciate the scaffold more after you’ve typed every config file yourself.
The Agenda
It’s a half-day build-along:
- Context and why this matters
- Repo setup and baseline structure
- Secret scanning with Gitleaks
- Shell linting with ShellCheck
- Semantic versioning with bump-version
- GitHub Actions CI
- Review and tuning
- The Shikomi reveal
- Wrap-up and Q&A
Secret scanning gets the longest block because it’s the one with the worst failure mode.
Four Ways to Load a Secret
The most reusable file in the repo is credential_patterns.sh, which shows four patterns for getting a secret into a script without hardcoding it:
| Pattern | When to use it |
|---|---|
| Jamf parameter | The secret arrives at runtime from Jamf; nothing is stored on the Mac |
.env file | Local development; never committed |
| macOS Keychain | A persistent service credential stored on this Mac |
| 1Password CLI | A shared team secret with a single source of truth and an audit trail |
One rule applies to all four: log secrets as *******, never the value. And one small bash habit that comes up in the file: read Jamf parameters as ${4:-}, because under set -u an unset parameter is a crash, not an empty string.
When the Secret Is Already in History
Gitleaks stops new commits. It doesn’t help with the repo you just inherited, or the one where someone used --no-verify two years ago. The cleanup guide walks through that scenario, and the order matters:
- Rotate the credential first. If the repo was ever pushed, even as a private repo, assume it was exposed. Scrubbing history is hygiene; rotation is the actual fix.
- Audit the full history once. Run Gitleaks with a JSON report so you know the whole scope before rewriting anything. Rewriting history repeatedly is worse than doing it once properly.
- Rewrite with
git filter-repo, notfilter-branch. You can replace the literal string across every commit, and the replacement text should explain itself (something like a note that the value was rotated and where the new one lives) rather than a bareREDACTED.
People skip step one because step three feels like the real work. It isn’t.
Rollback Is a Revert
Once scripts deploy from a repo, rollback has a standard shape. You git revert the bad commit, which preserves history and creates a new commit that undoes it. Then you bump the version with a message that says why (for example, a revert because a disk space check caused false positives on external drives), and the fix reaches Jamf when the revert merges.
The rollback guide also covers what to do mid-incident when you can’t wait for a CI run. The short version is that the revert-and-bump flow is the default, and anything faster should be a deliberate exception you write down afterward.
What’s in the Repo
The workshop repo has more than the walkthrough:
- A prep guide with a verification script, plus a guide for installing the tools without Homebrew
- A one-page command cheatsheet
- Example files to copy: a script with the full version header, an Extension Attribute example with its own conventions, a CI workflow, a deploy-to-Jamf workflow, an enhanced pre-commit config, a Gitleaks allowlist, a ShellCheck config, a CODEOWNERS file, and a contributor setup script
- Reference docs on pre-commit hooks, monorepo versus micro-repo tradeoffs, and team workflow (branch protection, CODEOWNERS, PR review, multi-environment deploys)
- An automated deployment guide that closes the loop from merge to Jamf Pro, with notes for other MDMs
Why Bother
A script that runs as root across a fleet is production code. It deserves a review, a version number, a changelog, and a scan for the password someone pasted in during a late-night fix. None of that is expensive once it’s wired up, and the first time Gitleaks blocks a commit that would have shipped a token, it has paid for the afternoon.
The materials are in the mac-gitops repo, the talk page is here, and Shikomi is the tool that scaffolds the whole setup.