Summary
The documented terminal install requires gh (logged in) and jq. That is a high barrier for a first-time user who just wants the CLI. Make the default terminal install need nothing beyond curl and a shell — without weakening first-install authenticity — by reusing the digest source the Unity GUI path already trusts: project-runner-pin.json.
Target UX:
# macOS / Linux / Windows Git Bash
curl -fsSL https://raw.githubusercontent.com/hatayama/unity-cli-loop/main/scripts/install.sh | sh
# Windows PowerShell
irm https://raw.githubusercontent.com/hatayama/unity-cli-loop/main/scripts/install.ps1 | iex
Current state
scripts/install.sh itself needs neither gh nor jq. Its dependencies are curl, shasum/sha256sum, and awk:
find_latest_asset_url (scripts/install.sh:284) resolves the newest matching release by scanning the releases API with curl + awk.
- Download and same-origin checksum comparison use
curl and shasum/sha256sum.
The single blocker is that a first install hard-fails without a trusted digest list: verify_archive_attestation_manifest (scripts/install.sh:485) exits with Attestation manifest is required when ULOOP_ARCHIVE_MANIFEST is unset (scripts/install.ps1:515 enforces the same). Producing that value is exactly what the README's gh attestation verify + jq block does. So gh/jq are not installer dependencies — they are the only currently documented way to supply one mandatory installer input.
That requirement must not simply be dropped: the .sha256 file lives in the same release as the archive, so an attacker who can replace one can replace both. ULOOP_ARCHIVE_MANIFEST is the authentication of a first install.
Proposal: source the digest manifest from the repository pin
Packages/src/project-runner-pin.json already carries dispatcherReleaseTag and dispatcherArchiveManifest, stamped by release automation and enforced by CI: VerifyDispatcherPinSubjects requires the manifest to match the published release's verified attestation subjects exactly (see docs/project-runner-pin.md, docs/dispatcher-pin-release-order.md). This is precisely why the Unity Install CLI button achieves full verification with zero extra tools.
Make the terminal path symmetrical:
- The user fetches
install.sh from the git repository (raw.githubusercontent.com, protected main), not from a release asset.
- When
ULOOP_ARCHIVE_MANIFEST is not supplied, the installer fetches Packages/src/project-runner-pin.json from the same repository ref it was itself fetched from, and uses dispatcherArchiveManifest as the trusted digest list and dispatcherReleaseTag as the version to install.
- An explicitly supplied
ULOOP_ARCHIVE_MANIFEST (the dispatcher self-update path, and the documented gh attestation verify flow) continues to take precedence unchanged.
- Neither present → fail, exactly as today.
Security analysis
- No new trust model. "Trust the repository contents over HTTPS" is already the accepted baseline of the Unity GUI path (package pin → digests). The terminal path merely becomes symmetrical with it; the system's weakest link does not move.
- Release-asset compromise is still detected. The digest list comes from the git repository (protected branch + CI verification), not from the release. An attacker who can rewrite release assets — installer, archives,
.sha256 — cannot make the repository-sourced manifest match. This is the decisive advantage over stamping digests into the release-asset installer, which that attacker could re-stamp consistently.
- Deterministic version. The installer stops resolving "latest" by default and installs the pinned
dispatcherReleaseTag — the same version the GUI path would install.
- Attestation flow remains the hardened option. The current
gh attestation verify block moves to an optional "verify the installer yourself" subsection for security-conscious users and CI.
Residual gap versus mandatory attestation: sigstore verification chains to the signing workflow run itself, repository trust chains to GitHub's branch protection. That gap already exists in the shipping GUI path, so this change introduces no regression anywhere.
Work involved
- Installer changes (
scripts/install.sh, scripts/install.ps1): pin fetch + parse (awk / native PowerShell JSON), env-var precedence, fail-closed when both sources are absent. Both files are shared release inputs — scripts/stamp-release-inputs.sh and the check-release-triggers CI gate apply (docs/shared-release-inputs.md).
- Decide the ref the installer reads the pin from: pinning to the installer's own fetched ref (
main in the documented command) is the natural choice; a ULOOP_REF override enables installing from a tag.
- README install section rewrite in both languages: the one-line default up front,
gh/jq prerequisites deleted, attestation flow demoted to an optional subsection.
- Windows parity per
docs/windows-compatibility.md. The PowerShell one-liner requires install.ps1 to stay executable when piped through iex (no top-level param block, no $PSScriptRoot dependence, configuration via environment variables only). The script already satisfies all three today — keep it that way and verify the irm | iex form end to end on Windows as part of acceptance.
- Tests: explicit manifest wins; pin-sourced manifest used when the env var is absent; both absent fails; pin digest mismatch fails; pin missing
dispatcherArchiveManifest fails.
Out of scope / follow-up
Homebrew tap and winget/scoop manifests share the same trust-separation structure (digests committed to a separate repository) and would further simplify installs, but they are distribution additions, not prerequisites for this fix.
Summary
The documented terminal install requires
gh(logged in) andjq. That is a high barrier for a first-time user who just wants the CLI. Make the default terminal install need nothing beyondcurland a shell — without weakening first-install authenticity — by reusing the digest source the Unity GUI path already trusts:project-runner-pin.json.Target UX:
Current state
scripts/install.shitself needs neitherghnorjq. Its dependencies arecurl,shasum/sha256sum, andawk:find_latest_asset_url(scripts/install.sh:284) resolves the newest matching release by scanning the releases API withcurl+awk.curlandshasum/sha256sum.The single blocker is that a first install hard-fails without a trusted digest list:
verify_archive_attestation_manifest(scripts/install.sh:485) exits withAttestation manifest is requiredwhenULOOP_ARCHIVE_MANIFESTis unset (scripts/install.ps1:515enforces the same). Producing that value is exactly what the README'sgh attestation verify+jqblock does. Sogh/jqare not installer dependencies — they are the only currently documented way to supply one mandatory installer input.That requirement must not simply be dropped: the
.sha256file lives in the same release as the archive, so an attacker who can replace one can replace both.ULOOP_ARCHIVE_MANIFESTis the authentication of a first install.Proposal: source the digest manifest from the repository pin
Packages/src/project-runner-pin.jsonalready carriesdispatcherReleaseTaganddispatcherArchiveManifest, stamped by release automation and enforced by CI:VerifyDispatcherPinSubjectsrequires the manifest to match the published release's verified attestation subjects exactly (seedocs/project-runner-pin.md,docs/dispatcher-pin-release-order.md). This is precisely why the Unity Install CLI button achieves full verification with zero extra tools.Make the terminal path symmetrical:
install.shfrom the git repository (raw.githubusercontent.com, protectedmain), not from a release asset.ULOOP_ARCHIVE_MANIFESTis not supplied, the installer fetchesPackages/src/project-runner-pin.jsonfrom the same repository ref it was itself fetched from, and usesdispatcherArchiveManifestas the trusted digest list anddispatcherReleaseTagas the version to install.ULOOP_ARCHIVE_MANIFEST(the dispatcher self-update path, and the documentedgh attestation verifyflow) continues to take precedence unchanged.Security analysis
.sha256— cannot make the repository-sourced manifest match. This is the decisive advantage over stamping digests into the release-asset installer, which that attacker could re-stamp consistently.dispatcherReleaseTag— the same version the GUI path would install.gh attestation verifyblock moves to an optional "verify the installer yourself" subsection for security-conscious users and CI.Residual gap versus mandatory attestation: sigstore verification chains to the signing workflow run itself, repository trust chains to GitHub's branch protection. That gap already exists in the shipping GUI path, so this change introduces no regression anywhere.
Work involved
scripts/install.sh,scripts/install.ps1): pin fetch + parse (awk/ native PowerShell JSON), env-var precedence, fail-closed when both sources are absent. Both files are shared release inputs —scripts/stamp-release-inputs.shand thecheck-release-triggersCI gate apply (docs/shared-release-inputs.md).mainin the documented command) is the natural choice; aULOOP_REFoverride enables installing from a tag.gh/jqprerequisites deleted, attestation flow demoted to an optional subsection.docs/windows-compatibility.md. The PowerShell one-liner requiresinstall.ps1to stay executable when piped throughiex(no top-levelparamblock, no$PSScriptRootdependence, configuration via environment variables only). The script already satisfies all three today — keep it that way and verify theirm | iexform end to end on Windows as part of acceptance.dispatcherArchiveManifestfails.Out of scope / follow-up
Homebrew tap and winget/scoop manifests share the same trust-separation structure (digests committed to a separate repository) and would further simplify installs, but they are distribution additions, not prerequisites for this fix.