GitHub Agentic Workflows

Frontmatter Hash Specification

Version: 1.0.0
Status: Draft
Publication Date: 2026-05-07
Latest Version: frontmatter-hash-specification
Editor: GitHub Agentic Workflows Team


This document specifies the algorithm for computing a deterministic hash of agentic workflow frontmatter, including contributions from imported workflows.

The frontmatter hash provides:

  1. Stale lock detection: Identify when the compiled lock file is out of sync with the source workflow (e.g. after editing the .md file without recompiling)
  2. Reproducibility: Ensure identical configurations produce identical hashes across languages (Go and JavaScript)
  3. Change detection: Verify that workflow configuration has not changed between compilation and execution
  • Basic Conformance: An implementation MUST compute a deterministic SHA-256 hash from canonicalized frontmatter input and MUST produce the same output for identical input.
  • Full Conformance: An implementation MUST satisfy Basic Conformance and MUST implement cross-language consistency checks between Go and JavaScript implementations.

The key words MUST, MUST NOT, REQUIRED, SHALL, SHALL NOT, SHOULD, SHOULD NOT, RECOMMENDED, MAY, and OPTIONAL in this document are to be interpreted as described in RFC 2119.

Collect all frontmatter from the main workflow and all imported workflows in breadth-first order (BFS traversal):

  1. Main workflow frontmatter: The frontmatter from the root workflow file
  2. Imported workflow frontmatter: Frontmatter from each imported file in BFS processing order
    • Includes transitively imported files (imports of imports)
    • Agent files (.github/agents/*.md) only contribute markdown content, not frontmatter

The BFS traversal processes imports level by level, starting from the root workflow. When a workflow imports multiple files, they are enqueued left-to-right in the order they appear in the imports: list. This ordering is preserved at every level.

Diamond-import handling: If a workflow file appears more than once in the import graph (a “diamond” dependency), the first occurrence in BFS order determines where that file’s frontmatter is merged; all subsequent occurrences of the same file MUST be silently skipped. Implementations MUST detect duplicate import paths using canonical path comparison (case-sensitive, no trailing-slash normalization) and discard duplicates without error.

Example (diamond graph):

root.md → imports: [a.md, b.md]
a.md → imports: [shared.md]
b.md → imports: [shared.md]

BFS queue order: [root.md, a.md, b.md, shared.md]
shared.md appears twice but is processed only once (after a.md in queue order).
Canonical hash input order: root → a → b → shared.

This rule ensures that the hash is deterministic regardless of which traversal path first discovers a shared dependency.

Include the following frontmatter fields in the hash computation:

Core Configuration:

  • engine - AI engine specification
  • on - Workflow triggers
  • permissions - GitHub Actions permissions
  • tracker-id - Workflow tracker identifier

Tool and Integration:

  • tools - Tool configurations (GitHub, Playwright, etc.)
  • mcp-servers - MCP server configurations
  • network - Network access permissions
  • safe-outputs - Safe output configurations
  • mcp-scripts - Safe input configurations

Runtime Configuration:

  • runtimes - Runtime version specifications (Node.js, Python, etc.)
  • services - Container services
  • cache - Caching configuration

Workflow Structure:

  • steps - Custom workflow steps
  • post-steps - Post-execution steps
  • jobs - GitHub Actions job definitions

Metadata:

  • description - Workflow description
  • labels - Workflow labels
  • bots - Authorized bot list
  • timeout-minutes - Workflow timeout
  • secret-masking - Secret masking configuration

Import Metadata:

  • imports - List of imported workflow paths (for traceability)
  • inputs - Input parameter definitions

Excluded Fields:

  • Markdown body content (not part of frontmatter)
  • Comments and whitespace variations
  • Field ordering (normalized during processing)

Transform the collected frontmatter into a canonical JSON representation:

For each workflow in BFS order:

  1. Parse frontmatter into a structured object
  2. Merge with accumulated frontmatter using these rules:
    • Replace: engine, on, tracker-id, description, timeout-minutes
    • Deep merge: tools, mcp-servers, network, permissions, runtimes, cache, services
    • Append: steps, post-steps, safe-outputs, mcp-scripts, jobs
    • Union: labels, bots (deduplicated)
    • Track: imports (list of all imported paths)

Apply these normalization rules to ensure deterministic output:

  1. Key Sorting: Sort all object keys alphabetically at every level
  2. Array Ordering: Preserve array order as-is (no sorting of array elements)
  3. Whitespace: Use minimal whitespace (no pretty-printing)
  4. Number Format: Represent numbers without exponents (e.g., 120 not 1.2e2)
  5. Boolean Values: Use lowercase true and false
  6. Null Handling: Include null values explicitly
  7. Empty Containers: Include empty objects {} and empty arrays []
  8. String Escaping: Use JSON standard escaping (quotes, backslashes, control characters)

The canonical JSON includes all frontmatter fields plus version information:

{
"bots": ["copilot"],
"cache": {},
"description": "Daily audit of workflow runs",
"engine": "claude",
"imports": ["shared/mcp/gh-aw.md", "shared/jqschema.md"],
"jobs": {},
"labels": ["audit", "automation"],
"mcp-servers": {},
"network": {"allowed": ["api.github.com"]},
"on": {"schedule": "daily"},
"permissions": {"actions": "read", "contents": "read"},
"post-steps": [],
"runtimes": {"node": {"version": "20"}},
"mcp-scripts": {},
"safe-outputs": {"create-discussion": {"category": "audits"}},
"services": {},
"steps": [],
"template-expressions": ["${{ env.MY_VAR }}"],
"timeout-minutes": 30,
"tools": {"repo-memory": {"branch-name": "memory/audit"}},
"tracker-id": "audit-workflows-daily",
"versions": {
"agents": "v0.0.84",
"awf": "v0.11.2",
"gh-aw": "dev"
}
}

The hash includes version numbers to ensure hash changes when dependencies are upgraded:

  • gh-aw: The compiler version (e.g., “0.1.0” or “dev”)
  • awf: The firewall version (e.g., “v0.11.2”)
  • agents: The MCP gateway version (e.g., “v0.0.84”)

This ensures that upgrading any component invalidates existing hashes.

  1. Serialize: Convert the merged and normalized frontmatter to canonical JSON
  2. Add Versions: Include version information for gh-aw, awf (firewall), and agents (MCP gateway)
  3. Hash: Compute SHA-256 hash of the JSON string (UTF-8 encoded)
  4. Encode: Represent the hash as a lowercase hexadecimal string (64 characters)

Example:

Input JSON: {"engine":"copilot","on":{"schedule":"daily"},"versions":{"agents":"v0.0.84","awf":"v0.11.2","gh-aw":"dev"}}
SHA-256: a1b2c3d4e5f6... (64 hex characters)

Both Go and JavaScript implementations MUST:

  • Use the same field selection and merging rules
  • Produce identical canonical JSON (byte-for-byte)
  • Use SHA-256 hash function
  • Encode output as lowercase hexadecimal

Test cases must verify identical hashes across both implementations for:

  • Empty frontmatter
  • Single-file workflows (no imports)
  • Multi-level imports (2+ levels deep)
  • All field types (strings, numbers, booleans, arrays, objects)
  • Special characters and escaping
  • All workflows in the repository

The current Go implementation (pkg/parser/frontmatter_hash.go) uses a text-based approach that diverges from the field-selection model described in Section 2 (“Field Selection”) of this specification:

  • Actual behavior: The entire normalized frontmatter text is hashed as a single opaque string (frontmatter-text key in the canonical JSON), alongside a sorted list of imported file paths and their normalized texts. This means all frontmatter fields — including excluded ones such as comments — affect the hash value.
  • Specified behavior: The specification calls for selecting individual named fields and merging them by type (replace, deep-merge, append, union).

Implication: The text-based approach is more conservative (any frontmatter change invalidates the hash, including whitespace-only changes after normalization) and simpler to implement cross-language. The trade-off is that it cannot support selective field exclusion without modifying the text normalization step.

Sync status (verified 2026-05-06): The Go implementation is consistent with the JavaScript implementation in actions/setup/js/ for the text-based approach. Both produce identical hashes for the same input. The field-selection model in Section 2 documents the logical intent; the text-based implementation is the authoritative runtime behavior until a future revision aligns them.

Resolution (2026-05-08): The project officially adopts the text-based approach as the authoritative runtime behavior (option b). Section 2 (“Field Selection”) documents the intended logical model for future alignment, but is non-normative until a dedicated migration milestone is scheduled. No immediate changes to the Go or JavaScript implementations are required. A future v2.0.0 revision of this specification MAY align both implementations to the field-selection model if selective field exclusion becomes a concrete requirement; that revision MUST include updated cross-language test vectors and a migration guide. Until then, implementations MUST continue to use the text-based approach and MUST NOT selectively exclude fields from the hash input.

  • Use crypto/sha256 for hashing (crypto/sha256.Sum256)
  • Use hex.EncodeToString() for hexadecimal encoding
  • Uses the same text-based approach as the Go implementation
  • Uses Node.js crypto.createHash('sha256') for hashing
  • Uses .digest('hex') for hexadecimal encoding
  • The JavaScript cross-language test suite in pkg/parser/frontmatter_hash_cross_language_test.go verifies identical output between the two implementations
  1. Compilation: The Go compiler computes the hash and writes it to the workflow log file
  2. Execution: The JavaScript custom action:
    • Reads the hash from the log file
    • Recomputes the hash from the workflow file
    • Compares the two hashes
    • Creates a GitHub issue if they differ (indicating frontmatter modification)

This section describes known risks associated with the frontmatter hash mechanism and the recommended mitigations.

SHA-256 produces a 256-bit output, giving a collision probability of approximately 2⁻¹²⁸ for any two distinct inputs under the birthday paradox. For the expected number of compiled workflows in a repository (typically <10,000), the probability of an accidental collision is negligible and does not require mitigation at the application layer.

However, implementations MUST NOT rely on the hash as a cryptographic commitment or security boundary. The hash is an integrity check for stale-lock detection only.

Mitigation: If future use cases require stronger collision resistance (e.g., content-addressed storage), implementations SHOULD upgrade to SHA-512 or SHA3-256 and bump the specification version.

The frontmatter hash detects accidental drift between the .md source and the compiled .lock.yml file. It does not prevent intentional tampering. Any user with write access to the repository can modify both files simultaneously:

  1. Edit the .md source.
  2. Recompile to regenerate the .lock.yml with the new hash.
  3. Commit both files in a single push.

This bypass is by design — the hash mechanism is intended to catch accidental stale locks, not to enforce a security boundary.

Mitigation: Enforce required code reviews via branch protection rules. Require signed commits for critical workflows. Use separate compilation and merge workflows with protected branches to prevent direct pushes to the default branch.

S-3: Inclusion of Sensitive Configuration in Hash Input

Section titled “S-3: Inclusion of Sensitive Configuration in Hash Input”

The canonical JSON used for hash computation includes all frontmatter fields, some of which may encode sensitive topology information (e.g., MCP server addresses in mcp-servers:, secret names in mcp-scripts:, or branch names in tools.repo-memory). This information is embedded in the .lock.yml file at compile time and is visible to anyone who can read the repository.

Mitigation: Treat repository visibility as the primary access control boundary. Avoid storing secret values in frontmatter (use GitHub Actions secrets instead). Periodically audit lock files for inadvertently committed sensitive configuration.

The hash includes versions.gh-aw, versions.awf, and versions.agents. Upgrading any of these components will invalidate all existing hashes, triggering stale-lock warnings on all workflows until they are recompiled. In a repository with many workflows, this can create a noisy wave of false-positive stale-lock issues.

Mitigation: Coordinate component upgrades with a bulk make recompile step. Automate recompilation in the upgrade PR so that lock files are always fresh after a version bump.

The Go and JavaScript implementations must produce byte-for-byte identical canonical JSON. Any divergence in key sorting, number representation, or null/undefined handling between the two implementations will cause the JavaScript runtime to report a false stale-lock mismatch for every workflow run.

Mitigation: Maintain a shared test-vector file (at minimum: empty frontmatter, single-field workflow, multi-level imports, all field types). Run cross-language hash tests in CI. Any change to the serialization algorithm in either language MUST be accompanied by updated test vectors verified against both implementations.

Very large frontmatter payloads can cause excessive memory use and hash-computation latency during compilation and runtime verification. This can degrade CI reliability and increase stale-lock false positives due to timeout or resource pressure.

Mitigation: Implementations SHOULD enforce a maximum cumulative frontmatter input size and MUST fail deterministically with a descriptive error when the limit is exceeded. A limit of 1 MiB for the combined normalized frontmatter input is RECOMMENDED unless repository-specific requirements justify a higher bound.


This section maps the frontmatter hash specification to the source files that implement it. Use this mapping to verify that specification changes are reflected in both implementations.

ComponentFile(s)
Go hash computationpkg/parser/frontmatter_hash.go (computeFrontmatterHashTextBased, computeFrontmatterHashTextBasedWithReader)
JavaScript hash computationactions/setup/js/frontmatter_hash.cjs
Cross-language testpkg/parser/frontmatter_hash_cross_language_test.go
Text normalizationpkg/parser/frontmatter_hash.go (normalizeFrontmatterText)
Import processingpkg/parser/frontmatter_hash.go (processImportsTextBased)

After any change to the hash algorithm:

  1. Update the Go implementation in pkg/parser/frontmatter_hash.go
  2. Update the JavaScript implementation in actions/setup/js/frontmatter_hash.cjs
  3. Run the cross-language test: go test ./pkg/parser/ -run TestFrontmatterHash
  4. Run make recompile to regenerate all lock files with fresh hashes
  5. Verify cross-language consistency for the test cases listed in Section 5

Runtime behavior: text-based approach is authoritative (see Implementation Notes § Resolution).

Resolution log (2026-05-12): Sync status re-verified after SPDD review. Approach retained: text-based canonicalization remains authoritative at runtime, and Section 2 field-selection stays documented as future-state design intent only.


  • The hash is not cryptographically secure for authentication (no HMAC/signing)
  • The hash is designed to detect stale lock files — it catches cases where the frontmatter has changed since the lock file was last compiled
  • The hash does not guarantee tamper protection: anyone with write access to the repository can modify both the .md source and the .lock.yml file together, bypassing detection
  • Always validate workflow sources through proper code review processes

This is version 1.0 of the frontmatter hash specification.

Future versions may:

  • Add additional fields
  • Change normalization rules
  • Use different hash algorithms

Version changes will be documented and backward compatibility maintained where possible.

Per the Resolution (2026-05-08) in Implementation Notes, the text-based algorithm remains authoritative until a dedicated migration milestone is approved.

Tracking issue: #31983

The project MUST NOT schedule a v2.0.0 migration to the field-selection model until all of the following tracked tasks are complete:

  • Confirm and document a selective field-exclusion use case in #31983.
  • Draft a migration guide in #31983, including lock-file invalidation and recompilation steps.
  • Write candidate v2.0.0 cross-language test vectors in #31983 and verify they pass in CI.
  • Approve a rollout plan in #31983, including backward-compatibility impact analysis.

Until these prerequisites are met, implementations MUST continue using the text-based algorithm and MUST NOT selectively exclude frontmatter fields from hash input.

Appendix A: Cross-Language Test Vectors (Text-Based Algorithm)

Section titled “Appendix A: Cross-Language Test Vectors (Text-Based Algorithm)”

The following vectors are normative for the current authoritative text-based algorithm.

Validation status: Each vector hash is verified to match in both implementations via automated cross-language tests in CI.

Expected hash: 4c8309afbcf816cd80c0824dce2b50047834b29e14b34b96953e88ae81048c46

This vector represents an intentionally empty frontmatter block (--- followed immediately by ---) rather than a file with no frontmatter delimiter. These are treated as different input forms for conformance testing and MUST be validated independently; this vector defines only the explicit-empty-block form.

---
---
# Empty Workflow

Expected hash: b9def9907e3328e2e03e8c47c315723df39788f251627313b1a984bb61b9cbce

---
engine: copilot
description: Test workflow
on:
schedule: daily
---
# Test Workflow

Expected hash: 8c63a05ef42cbfaff9be87a06257282cb4dcb952f71481d9d65ec3037003dbe8

---
engine: claude
description: Complex workflow
tracker-id: complex-test
timeout-minutes: 30
on:
schedule: daily
workflow_dispatch: true
permissions:
contents: read
actions: read
tools:
playwright:
version: v1.41.0
labels:
- test
- complex
bots:
- copilot
---
# Complex Workflow