sanad pins and updates GitHub Actions dependencies to immutable commit SHAs while preserving the logical refs you want to track.
Installation
Homebrew
On macOS or Linux with Homebrew:
brew tap MohamedElashri/sanad && brew install sanad
Check that it is installed
sanad version
Nix
Run the packaged release directly:
nix run github:MohamedElashri/sanad -- version
Or install it into your profile:
nix profile install github:MohamedElashri/sanad
Check that it is installed
sanad version
The flake installs the published release archive for your platform and verifies it with the release checksum.
Go
Install the latest tagged release with Go:
go install github.com/MohamedElashri/sanad/cmd/sanad@latest
Prebuilt archives
Tagged releases also publish Linux, macOS, and Windows archives on GitHub Releases. Download the archive for your platform, place the sanad binary on your PATH, and verify it against the published sanad_<version>_checksums.txt file.
Check the installed build:
sanad version
Quickstart
Scan workflows without making network calls:
sanad scan
Preview policy decisions and proposed pin updates:
GITHUB_TOKEN=$(gh auth token) sanad plan
Show the rewrite diff without changing files:
GITHUB_TOKEN=$(gh auth token) sanad apply --dry-run
Apply locally:
GITHUB_TOKEN=$(gh auth token) sanad apply --yes --write
Validate locally:
GITHUB_TOKEN=$(gh auth token) sanad check
sanad plan, sanad check, sanad apply, and sanad upgrade may contact GitHub when resolution is needed. sanad scan is local-only.
Human-readable output uses color automatically when the terminal supports it. Use --color never or NO_COLOR=1 to disable color, and --color always to force it for pagers or demos.
Example
Before:
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
After:
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # sanad: ref=v4
- uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # sanad: ref=v5
The workflow executes immutable SHAs. The comments and lockfile tell sanad which logical refs to resolve on future runs.
Scope
Sanad scans workflow files under .github/workflows by default, classifies uses: references, resolves GitHub tags and branches through the GitHub API, rewrites mutable action refs to full SHAs, adds # sanad: ref=... metadata, maintains .github/sanad.lock.json, applies cooldown rules, and emits table, JSON, SARIF, and Markdown helper output.
It is not a general dependency updater, vulnerability scanner, workflow formatter, YAML linter, Docker image updater, local action rewriter, or dedicated GitHub Action wrapper.
In Arabic scholarly culture, a sanad is a chain of transmission back to a source. This tool keeps that chain explicit for workflow dependencies: the workflow runs an immutable commit, while metadata records the tag or branch that commit came from.
Configuration
Create .sanad.toml when the defaults are not enough:
workflow_paths = [".github/workflows"]
cooldown = "14d"
cooldown_source = "source"
[updates]
tags = "track"
branches = "deny"
unpinned = "deny"
reusable_workflows = true
[ignore]
actions = [
"./*",
"docker://*"
]
files = []
[comments]
write = true
format = "sanad: ref={{ref}}"
[security]
require_full_sha = true
require_commit_in_source_repo = true
allow_private = true
deny_forks = false
Set [comments].write = false to rely on .github/sanad.lock.json without inline sanad comments. See the config reference for exact supported keys.
Interactive apply can optionally persist branch tracking by writing [updates].branches = "track" after final confirmation.
Commands
sanad scan
sanad check
sanad plan
sanad apply
sanad upgrade
sanad version
All commands accept:
--config .sanad.toml
--format table
--format json
sanad check --format sarif emits SARIF for code scanning, and sanad plan --pr-body-out body.md writes a Markdown pull request summary for automation.
sanad upgrade previews upgrades for all managed pins to their latest GitHub release. It is dry-run by default; add --write after reviewing the diff.
sanad upgrade --action actions/checkout --to v5 intentionally moves one managed pin to a specific logical ref while keeping workflow execution pinned to a full SHA.
Command-specific usage is covered in the CLI reference.
GitHub Authentication
sanad reads tokens from environment variables:
GITHUB_TOKEN
GH_TOKEN
Tokens are used for GitHub API requests and are never printed by the CLI. Public repositories can work without a token, but authenticated requests are strongly recommended for CI and private repositories.
For local shell usage, prefer reusing the GitHub CLI token instead of pasting a token into your terminal:
GITHUB_TOKEN=$(gh auth token) sanad plan
Security Model
The core policy is simple: workflow dependencies should run immutable full-length SHAs. Mutable tags and branches are resolved to commits, short SHAs are rejected, local and Docker actions are skipped by default, and branch or unpinned behavior must be explicitly allowed before it is managed non-interactively.
See the security model for the full model.
Cooldown
The default cooldown is 14d, and the default cooldown_source = "source" uses the upstream release, tag, or commit timestamp. This keeps routine updates moving while still rejecting missing or future timestamps. Set cooldown_source = "first-seen" when you want the stricter mode: Sanad records when a candidate SHA was first seen locally and waits for that observation window before adopting it.
CI
Example enforcement job:
name: Check pinned actions
on:
pull_request:
push:
branches: [main]
jobs:
sanad:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
- uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff
with:
go-version: "1.26.x"
- run: go install github.com/MohamedElashri/sanad/cmd/sanad@latest
- run: sanad check --format json
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
See the CI guide for update workflows and pull request automation.
Development
make build
make test
make lint
make docs-build
The documentation site is built with Nida from docs/. User docs live under docs/content/guide, exact lookup pages under docs/content/reference, and contributor/internal docs under docs/content/advanced.
LICENCE
This project is released under the MIT LICENCE