Makoto
Menu
Case 06 · invisible Unicode

Unchanged bytes can still violate policy.

A source file renders exactly like a safe one, but carries 128 invisible variation selectors that encode hidden content. Its digest is honest and its signature is valid, because the dangerous bytes were present when the producer signed them.

The code this page is about. Repeated from The output that catches it below.
$ makoto verify bundle handoffs/flagged-invisible/bundle \
  --policy receiver/policy.json \
  --schema-catalog receiver/catalog.json \
  --expected-manifest sha256:ad9c3c7abc51c1dee1fb3939e9ef23c4345203bd1c96a3f2c5ccdff74737e89e \
  --expected-head sha256:6fdf6d7bcbdbf7ed7b1b22d9ccee5e9cb1a09bb16ffa884edf30f409310ec274 \
  --expected-artifact receiver/expected/flagged-invisible.artifact.json \
  --evaluation-time 2026-09-16T16:00:00Z
PASS        load-safely
PASS        parse-strictly
PASS        index-payloads
PASS        core-schemas
PASS        signatures
FAIL        authorization-thresholds
PASS        metadata-profiles
FAIL        authorization
PASS        graph-dependency-artifacts
SKIPPED     graph
SKIPPED     roots-and-heads
FAIL        completeness-anchor
PASS        freshness-anchors
SKIPPED     artifact-bytes
SKIPPED     artifact-profiles
PASS        decision
DENY        decision
manifest sha256:ad9c3c7abc51c1dee1fb3939e9ef23c4345203bd1c96a3f2c5ccdff74737e89e
E_SIGNER_UNAUTHORIZED: no matching policy rule met its signature threshold
E_MANIFEST_SET: final artifact is not an exact terminal subject of its declared head
E_MANIFEST_SET: signed manifest sets do not equal the verified graph and bundle index
$ echo $?
1

Evidence boundary. Every terminal block on this page was captured by running the command shown against the fixtures checked in beside this page, with the Makoto core verifier at commit 0e41afb. The pinned core commit 276008e adds verification summaries, a separately dispatched newer protocol version, and keyless signing without changing these results, and the gate below runs it. Verification-summary tables are computed from the published reports with its property rules. The verification reports are byte-for-byte the published allow and deny fixtures. Hidden characters are shown escaped, never embedded in this page.

The problem

Package-registry campaigns have shipped code whose rendered view and byte view disagree. Variation selectors, characters meant to be invisible, encode instructions that a runtime decoder reconstructs. The code looks normal in every editor and review tool.

Those packages can be signed, their hashes valid, and their reviews passed. Provenance and integrity checks answer "are these the bytes that were vouched for?" and the answer is yes. The question nobody asked was whether the bytes a reviewer approved are the bytes a reviewer saw.

  1. The raw example

    The same line from the safe file and from the flagged file. With the hidden codepoints stripped, as a reviewer's editor effectively shows it, the flagged line is identical. Escaped, it is not.

    $ sed -n 5p src/safe-visible.js
      if (!record || typeof record !== "object") {
    $ sed -n 6p src/flagged-invisible.js | uv run --no-project python -c \
      'import re, sys; print(re.sub("[\\ufe00-\\ufe0f\\U000e0100-\\U000e01ef]", "", sys.stdin.read()), end="")'
      if (!record || typeof record !== "object") {
    $ sed -n 6p src/flagged-invisible.js | uv run --no-project python -c \
      'import sys; print(ascii(sys.stdin.read().rstrip()))'
    '  if (!r\ufe04\U000e0105eco\ufe08\U000e0106rd || t\ufe09\U000e0106ype\ufe03\U000e0107of r\ufe00\U000e0102eco\ufe04\U000e0107rd !== "o\ufe05\U000e0106bje\ufe08\U000e0107ct") {'

    Download safe-visible.js, flagged-invisible.js, and the annotated revealed view. The hidden content decodes only to an inert explanation string.

  2. How it failed

    The flagged file is 508 bytes longer than the safe one for the same visible text. The difference is 128 codepoints in two variation-selector ranges. A render-safety scan records exactly that, with verdict fail.

    $ wc -c src/safe-visible.js src/flagged-invisible.js
         710 src/safe-visible.js
        1218 src/flagged-invisible.js
        1928 total
    $ shasum -a 256 src/*.js
    0dba71c75cdeefbbe428436db262721ac91543266c460920d76c2fa55c27d85c  src/flagged-invisible.js
    d7dd00740f0860f7f01369cdd036674ddccabba65e702dfc8d40435240fcd46f  src/safe-visible.js
    $ uv run --no-project python - src/flagged-invisible.js <<'PY'
    import sys, unicodedata
    text = open(sys.argv[1], encoding='utf-8').read()
    hidden = [c for c in text if 0xFE00 <= ord(c) <= 0xFE0F or 0xE0100 <= ord(c) <= 0xE01EF]
    print('codepoints:', len(text), 'hidden variation selectors:', len(hidden))
    print('first five:', ' '.join(f'U+{ord(c):04X}' for c in hidden[:5]))
    PY
    codepoints: 896 hidden variation selectors: 128
    first five: U+FE04 U+E0105 U+FE08 U+E0106 U+FE09
    $ jq '.["urn:makoto:demo:render-safe"] | {verdict, contains_invisible_codepoints, counts, ranges}' scanner/flagged-invisible.scan.json
    {
      "verdict": "fail",
      "contains_invisible_codepoints": true,
      "counts": {
        "invisible_codepoints": 128,
        "total_codepoints": 896
      },
      "ranges": [
        "U+FE00-U+FE0F",
        "U+E0100-U+E01EF"
      ]
    }

    Read the full scan output in analysis.flagged.json.

  3. Put Makoto in the path

    A pull request that changes a source handoff runs the render-safety gate. The receiver's policy authorizes origin claims only when they carry a passing scan, so a scan result is not advisory: it decides whether the claim can be authorized at all.

    The workflow

    # Render-safety gate for source files. The receiver policy only
    # authorizes origin claims that carry a passing render-safety scan.
    #
    # Copy to .github/workflows/makoto-gate.yml in the repository that
    # receives handoffs. Layout it expects:
    #   handoffs/<name>/bundle/                  what the sender transferred
    #   receiver/policy.json                     receiver-owned trust policy
    #   receiver/catalog.json, receiver/resources/  pinned private schemas
    #   receiver/expected/<name>.artifact.json   expected final artifact
    name: Makoto render-safety gate
    
    on:
      workflow_dispatch:
      pull_request:
        paths:
          - "handoffs/**"
    
    permissions:
      contents: read
    
    env:
      # Pinned so these fixtures replay identically. Delete this line to
      # evaluate against the runner clock.
      EVALUATION_TIME: "2026-09-16T16:00:00Z"
    
    jobs:
      gate:
        runs-on: ubuntu-latest
        strategy:
          fail-fast: false
          matrix:
            include:
              # The sender publishes manifest and head digests out of band.
              # The receiver pins them here, never inside the bundle.
              - handoff: safe-visible
                manifest: sha256:a1020fcb3d691dbbad9d6567c1d33bea36b96a4b99c6282e8958d59472bdb1f6
                head: sha256:44ca1c22a2fd7f3b58f407509b5a9fd933b57d5749786e3256154f1e3d1b82a3
              - handoff: flagged-invisible
                manifest: sha256:ad9c3c7abc51c1dee1fb3939e9ef23c4345203bd1c96a3f2c5ccdff74737e89e
                head: sha256:6fdf6d7bcbdbf7ed7b1b22d9ccee5e9cb1a09bb16ffa884edf30f409310ec274
        steps:
          - uses: actions/checkout@v4
          # Pin the verifier to an exact core commit, like any dependency.
          # Run it from a checkout: the CLI resolves its core schemas from
          # the repository tree. This is the core commit the hosted
          # schemas are pinned to.
          - uses: actions/checkout@v4
            with:
              repository: makoto-project/makoto
              ref: 276008eeec0f10d5322c4c627b8c103222fee841
              path: .makoto-core
              persist-credentials: false
          - uses: astral-sh/setup-uv@v6
          - name: Verify the received bundle
            id: verify
            shell: bash
            env:
              HANDOFF: ${{ matrix.handoff }}
              EXPECTED_MANIFEST: ${{ matrix.manifest }}
              EXPECTED_HEAD: ${{ matrix.head }}
            run: |
              verify() {
                uv run --project .makoto-core makoto verify bundle "handoffs/$HANDOFF/bundle" \
                  --policy receiver/policy.json \
                  --schema-catalog receiver/catalog.json \
                  --expected-manifest "$EXPECTED_MANIFEST" \
                  --expected-head "$EXPECTED_HEAD" \
                  --expected-artifact "receiver/expected/$HANDOFF.artifact.json" \
                  --evaluation-time "$EVALUATION_TIME" "$@"
              }
              status=0
              verify --json > makoto-report.json || status=$?
              verify || true
              exit "$status"
          - name: Keep the verification report
            if: always()
            uses: actions/upload-artifact@v4
            with:
              name: makoto-report-${{ matrix.handoff }}
              path: makoto-report.json
    
      merge-check:
        # Runs only when every handoff in the matrix was allowed.
        needs: gate
        runs-on: ubuntu-latest
        steps:
          - uses: actions/checkout@v4
          - name: Report the render-safety result
            run: echo "replace with your existing step"

    Download makoto-gate.yml. The matrix runs the safe and flagged handoffs through the same step.

    The gate keeps the JSON report as a build artifact, so a later job can summarize it into a signed verification summary without running verification again.

    The render-safety profile and the rule that requires it

    $ jq . receiver/resources/*.schema.json
    {
      "$schema": "https://usemakoto.dev/schema/v0.2/profile-dialect.schema.json",
      "$id": "https://schemas.example.internal/makoto/render-safe-origin-v1.schema.json",
      "type": "object",
      "required": [
        "schemaVersion",
        "event",
        "source",
        "extensions"
      ],
      "properties": {
        "schemaVersion": {
          "const": "0.2"
        },
        "extensions": {
          "type": "object",
          "required": [
            "urn:makoto:demo:render-safe"
          ],
          "properties": {
            "urn:makoto:demo:render-safe": {
              "type": "object",
              "required": [
                "scanner",
                "normalization",
                "contains_invisible_codepoints",
                "verdict"
              ],
              "properties": {
                "normalization": {
                  "const": "NFC"
                },
                "contains_invisible_codepoints": {
                  "const": false
                },
                "verdict": {
                  "const": "pass"
                }
              }
            }
          }
        }
      }
    }
    $ jq '.rules[] | {id, authorizedKeyIds, profileConstraints}' receiver/policy.json
    {
      "id": "urn:makoto:demo:rule:render-safe-origin",
      "authorizedKeyIds": [
        "sha256:472887aa21e3c36ab999ea1661e03cf232fca7957f1e502698216a8bf1185a01"
      ],
      "profileConstraints": [
        {
          "closureDigest": {
            "sha256": "14a57a9dbc837c5c904cb7aa4bc9d23e80b4696fa70e9cdd9d6e10add499457c"
          },
          "digest": {
            "sha256": "bd43c68df4621b055f6f2a5d4e5069331206f7fe78a1eda240064259b31ce508"
          },
          "id": "https://schemas.example.internal/makoto/render-safe-origin-v1.schema.json",
          "target": "predicate"
        }
      ]
    }

    The schema is render-safe-origin-v1.schema.json; the full policy is receiver-policy.json.

    The producer cannot attach a profile it does not meet

    Makoto enforces the profile at signing time too. Asking the scanner to sign the flagged file's origin claim with the render-safety profile attached is refused, with exit status 2, before any envelope is written.

    $ makoto attest origin \
      --subject flagged-invisible.js=src/flagged-invisible.js \
      --source-kind urn:makoto:demo:source:repository-file \
      --source-uri urn:makoto:demo:v0.2:source:flagged-invisible-js \
      --extensions scanner/flagged-invisible.scan.json \
      --event-id urn:uuid:06000002-0600-4600-8600-060000000002 \
      --occurred-at 2026-09-16T16:00:00Z \
      --key scanner/keys/scanner.private.pem \
      --profile receiver/render-safe.profile.json \
      --schema-catalog receiver/catalog.json \
      --out scanner/origin.dsse.json
    {"errorClass":"invalid-input","message":"profile claim is false: False was expected; 'pass' was expected"}
    $ echo $?
    2
  4. The output that catches it

    The only way to sign the flagged claim is without the profile. That envelope is published as attestation.flagged.json. Its signature verifies, but the only origin rule requires the render-safety profile, so no rule authorizes the statement. The deny is E_SIGNER_UNAUTHORIZED, not a digest or signature error: the bytes were never tampered with, they were never acceptable.

    $ makoto verify bundle handoffs/flagged-invisible/bundle \
      --policy receiver/policy.json \
      --schema-catalog receiver/catalog.json \
      --expected-manifest sha256:ad9c3c7abc51c1dee1fb3939e9ef23c4345203bd1c96a3f2c5ccdff74737e89e \
      --expected-head sha256:6fdf6d7bcbdbf7ed7b1b22d9ccee5e9cb1a09bb16ffa884edf30f409310ec274 \
      --expected-artifact receiver/expected/flagged-invisible.artifact.json \
      --evaluation-time 2026-09-16T16:00:00Z
    PASS        load-safely
    PASS        parse-strictly
    PASS        index-payloads
    PASS        core-schemas
    PASS        signatures
    FAIL        authorization-thresholds
    PASS        metadata-profiles
    FAIL        authorization
    PASS        graph-dependency-artifacts
    SKIPPED     graph
    SKIPPED     roots-and-heads
    FAIL        completeness-anchor
    PASS        freshness-anchors
    SKIPPED     artifact-bytes
    SKIPPED     artifact-profiles
    PASS        decision
    DENY        decision
    manifest sha256:ad9c3c7abc51c1dee1fb3939e9ef23c4345203bd1c96a3f2c5ccdff74737e89e
    E_SIGNER_UNAUTHORIZED: no matching policy rule met its signature threshold
    E_MANIFEST_SET: final artifact is not an exact terminal subject of its declared head
    E_MANIFEST_SET: signed manifest sets do not equal the verified graph and bundle index
    $ echo $?
    1

    The machine-readable report

    Output of jq '{decision, primaryError, errors, notPassed: [.checks[] | select(.status != "pass") | {id, status}]}' makoto-report.flagged-invisible.json against the report the workflow step wrote.

    {
      "decision": "deny",
      "primaryError": "E_SIGNER_UNAUTHORIZED",
      "errors": [
        {
          "causedByCheck": "authorization-thresholds",
          "code": "E_SIGNER_UNAUTHORIZED",
          "context": {
            "statementDigest": {
              "sha256": "6fdf6d7bcbdbf7ed7b1b22d9ccee5e9cb1a09bb16ffa884edf30f409310ec274"
            }
          },
          "message": "no matching policy rule met its signature threshold",
          "step": 6
        },
        {
          "causedByCheck": "completeness-anchor",
          "code": "E_MANIFEST_SET",
          "context": {
            "artifactDigest": {
              "sha256": "0dba71c75cdeefbbe428436db262721ac91543266c460920d76c2fa55c27d85c"
            },
            "bundleId": "urn:makoto:demo:06:flagged",
            "head": {
              "sha256": "6fdf6d7bcbdbf7ed7b1b22d9ccee5e9cb1a09bb16ffa884edf30f409310ec274"
            },
            "subjectName": "flagged-invisible.js"
          },
          "message": "final artifact is not an exact terminal subject of its declared head",
          "step": 11
        },
        {
          "causedByCheck": "completeness-anchor",
          "code": "E_MANIFEST_SET",
          "context": {
            "bundleId": "urn:makoto:demo:06:flagged"
          },
          "message": "signed manifest sets do not equal the verified graph and bundle index",
          "step": 11
        }
      ],
      "notPassed": [
        {
          "id": "authorization-thresholds",
          "status": "fail"
        },
        {
          "id": "authorization",
          "status": "fail"
        },
        {
          "id": "graph",
          "status": "skipped"
        },
        {
          "id": "roots-and-heads",
          "status": "skipped"
        },
        {
          "id": "completeness-anchor",
          "status": "fail"
        },
        {
          "id": "artifact-bytes",
          "status": "skipped"
        },
        {
          "id": "artifact-profiles",
          "status": "skipped"
        }
      ]
    }

    What a verification summary records

    A verification summary over this report is FAILED with E_REPORT_DENIED, because the decision is deny. Its evaluation sidecar still records each verified property's exact rule against the report's checks:

    PropertyWhat its rule reads in this reportResult
    MAKOTO_AUTHORIZEDsignatures pass, authorization-thresholds fail, authorization fail; handoff authorization passNot established: E_PROPERTY_AUTHORIZED
    MAKOTO_GRAPH_COMPLETEindex-payloads pass, graph-dependency-artifacts pass, graph skipped, roots-and-heads skipped, completeness-anchor failNot established: E_PROPERTY_GRAPH_COMPLETE
    MAKOTO_FRESHNESS_ANCHOREDfreshness-anchors pass; methods: expected-artifacts pass, expected-heads pass, expected-manifest pass, max-age not_checked, nonce not_checkedEstablished
    MAKOTO_SCHEMA_CONFORMANTcore-schemas pass, metadata-profiles pass, artifact-profiles skipped; the policy requires 1 digest-pinned profileNot established: E_PROPERTY_SCHEMA_CONFORMANT
    MAKOTO_REPRODUCEDNeeds a second allow report from an independent Transform L2-or-higher platform; none is suppliedNot established: E_PROPERTY_REPRODUCED

    Computed from the published report and the policy whose digest it records, with the property rules in core at the pinned commit. Track levels are not shown: they also need the statements themselves and any platform assessments.

    Open the complete flagged report.

    Control run: the safe file through the same gate
    $ makoto verify bundle handoffs/safe-visible/bundle \
      --policy receiver/policy.json \
      --schema-catalog receiver/catalog.json \
      --expected-manifest sha256:a1020fcb3d691dbbad9d6567c1d33bea36b96a4b99c6282e8958d59472bdb1f6 \
      --expected-head sha256:44ca1c22a2fd7f3b58f407509b5a9fd933b57d5749786e3256154f1e3d1b82a3 \
      --expected-artifact receiver/expected/safe-visible.artifact.json \
      --evaluation-time 2026-09-16T16:00:00Z
    PASS        load-safely
    PASS        parse-strictly
    PASS        index-payloads
    PASS        core-schemas
    PASS        signatures
    PASS        authorization-thresholds
    PASS        metadata-profiles
    PASS        authorization
    PASS        graph-dependency-artifacts
    PASS        graph
    PASS        roots-and-heads
    PASS        completeness-anchor
    PASS        freshness-anchors
    PASS        artifact-bytes
    PASS        artifact-profiles
    PASS        decision
    ALLOW       decision
    manifest sha256:a1020fcb3d691dbbad9d6567c1d33bea36b96a4b99c6282e8958d59472bdb1f6
    $ echo $?
    0

    What a verification summary records

    A verification summary over this allow report records each verified property's exact rule against its checks:

    PropertyWhat its rule reads in this reportResult
    MAKOTO_AUTHORIZEDsignatures pass, authorization-thresholds pass, authorization pass; handoff authorization passEstablished
    MAKOTO_GRAPH_COMPLETEindex-payloads pass, graph-dependency-artifacts pass, graph pass, roots-and-heads pass, completeness-anchor passEstablished
    MAKOTO_FRESHNESS_ANCHOREDfreshness-anchors pass; methods: expected-artifacts pass, expected-heads pass, expected-manifest pass, max-age not_checked, nonce not_checkedEstablished
    MAKOTO_SCHEMA_CONFORMANTcore-schemas pass, metadata-profiles pass, artifact-profiles pass; the policy requires 1 digest-pinned profileEstablished
    MAKOTO_REPRODUCEDNeeds a second allow report from an independent Transform L2-or-higher platform; none is suppliedNot established: E_PROPERTY_REPRODUCED

    Computed from the published report and the policy whose digest it records, with the property rules in core at the pinned commit. Track levels are not shown: they also need the statements themselves and any platform assessments.

    Open the complete safe report.

The correct failure

  1. Verify authenticity Keep the valid signature result. Do not mislabel the content hazard as tampering.
  2. Verify artifact integrity Keep the valid digest result when the received bytes are unchanged.
  3. Apply the required profile Validate the actual structured artifact or relevant predicate extension under the receiver's digest-pinned schema.
  4. Deny on content policy Report the exact rule that failed so the producer can normalize, reject, or explicitly justify the content.

Why this belongs in the provenance story

The profile and result travel with the evidence boundary, so a later recipient can tell the difference between “bytes changed” and “unchanged bytes were never acceptable.” That distinction matters for incident response and remediation.

Concrete content-policy check

InputA JavaScript source file whose visible text matches a safe file but contains 128 invisible variation selectors.
Integrity resultThe digest and signature pass because the bytes have not changed since signing.
Profile resultThe producer cannot attach the receiver's render-safety profile, and the receiver's only origin rule requires it, so the claim is not authorized.
RemediationThe producer removes the hidden codepoints, re-scans, creates new output bytes, and signs a new origin claim that carries the passing scan.

Inspect the safe and flagged byte sequences side by side

What else this handles

  • Zero-width characters hiding content in configuration files and prompts.
  • Bidirectional control abuse that makes code read differently from how it executes.
  • Tag-character sequences embedding data invisible to standard rendering.
  • Any Unicode family where the rendered view diverges from the byte content.

Key insight. Provenance tells you where an artifact came from. Signatures tell you who vouched for it. Render-safety analysis tells you what is actually present in the bytes, even when a reviewer cannot see it. A receiver needs all three.

See also the display-layer obfuscation threat.

Inspect the bytes

The detailed case includes a bounded string-profile example.