{"repo":"NousResearch/hermes-agent","public_id":"27397","display_label":"Windows install.ps1 script bugs and fixes","summary":"Multiple bugs and fixes related to the Windows PowerShell installer script, including a malformed param() block, UTF-8 BOM characters, and syntax errors that break the one-line installation command. Several fixes address the same or closely related root causes.","subsystem":"hermes_cli","counts":{"members":8,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":4,"merged_prs":1,"attempt_prs":3},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":28169},"resolution":{"state":"unresolved","by":null},"freshness":{"state":"verified","last_verified_at":"2026-05-19T01:28:41+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_PARTIAL","link_density":0.2557142857142857},"fix_edges":[{"issue_number":27397,"pr_number":28169,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false}],"members":[{"kind":"issue","number":27397,"title":"[Bug]: One Line Install not working","url":null,"state":"closed"},{"kind":"issue","number":27622,"title":"[Bug]: Windows Install Isn't Working","url":null,"state":"closed"},{"kind":"issue","number":28071,"title":"Fix critical syntax error in install.ps1 that breaks Windows installation","url":null,"state":"closed"},{"kind":"issue","number":28141,"title":"install.ps1: malformed param() block breaks Windows one-line installer","url":null,"state":"closed"},{"kind":"pr","number":27419,"title":"fix(install.ps1): drop UTF-8 BOM that breaks the irm | iex one-liner (#27397)","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#28169","number":28169},{"kind":"pr","value":"#28169","number":28169}],"category":"superseded_by_pr","implication":"superseded"},"merged":false},{"kind":"pr","number":28169,"title":"feat(install.ps1): strip BOM, add -Commit/-Tag pin params, harden git ops","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["scripts/install.ps1"],"fix_hunks":[{"file":"scripts/install.ps1","hunks":["@@ -1,4 +1,4 @@\n-﻿# ============================================================================\n+# ============================================================================\n # Hermes Agent Installer for Windows\n # ============================================================================\n # Installation script for Windows (PowerShell).","@@ -16,6 +16,13 @@ param(\n     [switch]$NoVenv,\n     [switch]$SkipSetup,\n     [string]$Branch = \"main\",\n+    # -Commit and -Tag are higher-precedence variants of -Branch for users\n+    # who need reproducible installs (desktop installer pinning, CI, release\n+    # bundles).  When set, the repository stage clones $Branch (faster than\n+    # cloning the full default-branch history) and then `git checkout`s the\n+    # exact ref.  Precedence: Commit > Tag > Branch.\n+    [string]$Commit = \"\",\n+    [string]$Tag = \"\",\n     [string]$HermesHome = \"$env:LOCALAPPDATA\\hermes\",\n     [string]$InstallDir = \"$env:LOCALAPPDATA\\hermes\\hermes-agent\",\n ","@@ -955,14 +962,36 @@ function Install-Repository {\n         if ($repoValid) {\n             Write-Info \"Existing installation found, updating...\"\n             Push-Location $InstallDir\n+            # Wrap the entire fetch+checkout block in EAP=Continue so git's\n+            # routine stderr output (e.g. 'From <url>' info lines emitted by\n+            # `git fetch`) doesn't terminate the script under the global\n+            # EAP=Stop.  We rely on $LASTEXITCODE for actual failures.\n+            $prevEAP = $ErrorActionPreference\n+            $ErrorActionPreference = \"Continue\"\n             try {\n                 git -c windows.appendAtomically=false fetch origin\n                 if ($LASTEXITCODE -ne 0) { throw \"git fetch failed (exit $LASTEXITCODE)\" }\n-                git -c windows.appendAtomically=false checkout $Branch\n-                if ($LASTEXITCODE -ne 0) { throw \"git checkout $Branch failed (exit $LASTEXITCODE)\" }\n-                git -c windows.appendAtomically=false pull origin $Branch\n-                if ($LASTEXITCODE -ne 0) { throw \"git pull failed (exit $LASTEXITCODE)\" }\n+                # Precedence: Commit > Tag > Branch.  Commit and Tag check\n+                # out as detached HEAD intentionally -- they're meant to be\n+                # reproducible pins, not branches the user pulls into.\n+                if ($Commit) {\n+                    # Make sure we have the commit locally (a tag-less commit\n+                    # SHA isn't always reachable from any one branch fetch).\n+                    git -c windows.appendAtomically=false fetch origin $Commit\n+                    git -c windows.appendAtomically=false checkout --detach $Commit\n+                    if ($LASTEXITCODE -ne 0) { throw \"git checkout $Commit failed (exit $LASTEXITCODE)\" }\n+                } elseif ($Tag) {\n+                    git -c windows.appendAtomically=false fetch origin \"refs/tags/${Tag}:refs/tags/${Tag}\"\n+                    git -c windows.appendAtomically=false checkout --detach \"refs/tags/$Tag\"\n+                    if ($LASTEXITCODE -ne 0) { throw \"git checkout tag $Tag failed (exit $LASTEXITCODE)\" }\n+                } else {\n+                    git -c windows.appendAtomically=false checkout $Branch\n+                    if ($LASTEXITCODE -ne 0) { throw \"git checkout $Branch failed (exit $LASTEXITCODE)\" }\n+                    git -c windows.appendAtomically=false pull origin $Branch\n+                    if ($LASTEXITCODE -ne 0) { throw \"git pull failed (exit $LASTEXITCODE)\" }\n+                }\n             } finally {\n+                $ErrorActionPreference = $prevEAP\n                 Pop-Location\n             }\n             $didUpdate = $true","@@ -1020,8 +1049,20 @@ function Install-Repository {\n             if (Test-Path $InstallDir) { Remove-Item -Recurse -Force $InstallDir -ErrorAction SilentlyContinue }\n             Write-Warn \"Git clone failed -- downloading ZIP archive instead...\"\n             try {\n-                $zipUrl = \"https://github.com/NousResearch/hermes-agent/archive/refs/heads/$Branch.zip\"\n-                $zipPath = \"$env:TEMP\\hermes-agent-$Branch.zip\"\n+                # Pick the ZIP URL for the most-specific ref the caller asked\n+                # for.  GitHub supports archive URLs for commits, tags, and\n+                # branches; we honour Commit > Tag > Branch.\n+                if ($Commit) {\n+                    $zipUrl = \"https://github.com/NousResearch/hermes-agent/archive/$Commit.zip\"\n+                    $zipLabel = $Commit\n+                } elseif ($Tag) {\n+                    $zipUrl = \"https://github.com/NousResearch/hermes-agent/archive/refs/tags/$Tag.zip\"\n+                    $zipLabel = $Tag\n+                } else {\n+                    $zipUrl = \"https://github.com/NousResearch/hermes-agent/archive/refs/heads/$Branch.zip\"\n+                    $zipLabel = $Branch\n+                }\n+                $zipPath = \"$env:TEMP\\hermes-agent-$zipLabel.zip\"\n                 $extractPath = \"$env:TEMP\\hermes-agent-extract\"\n \n                 Invoke-WebRequest -Uri $zipUrl -OutFile $zipPath -UseBasicParsing","@@ -1063,6 +1104,37 @@ function Install-Repository {\n     Push-Location $InstallDir\n     git -c windows.appendAtomically=false config windows.appendAtomically false 2>$null\n \n+    # Post-clone pin: when a clone (or ZIP-fallback init) just landed us on\n+    # $Branch's tip, honour the higher-precedence $Commit / $Tag by checking\n+    # the exact ref out as a detached HEAD.  Skipped for the in-place update\n+    # path (above) since that already routed via the same precedence.\n+    if (-not $didUpdate) {\n+        # Same EAP=Continue wrap as the update path -- git fetch's 'From <url>'\n+        # info line goes to stderr and would terminate the script under the\n+        # global EAP=Stop otherwise.  We check $LASTEXITCODE for real errors.\n+        $prevEAP = $ErrorActionPreference\n+        $ErrorActionPreference = \"Continue\"\n+        try {\n+            if ($Commit) {\n+                Write-Info \"Pinning to commit $Commit...\"\n+                git -c windows.appendAtomically=false fetch origin $Commit\n+                git -c windows.appendAtomically=false checkout --detach $Commit\n+                if ($LASTEXITCODE -ne 0) {\n+                    throw \"git checkout $Commit failed (exit $LASTEXITCODE)\"\n+                }\n+            } elseif ($Tag) {\n+                Write-Info \"Pinning to tag $Tag...\"\n+                git -c windows.appendAtomically=false fetch origin \"refs/tags/${Tag}:refs/tags/${Tag}\"\n+                git -c windows.appendAtomically=false checkout --detach \"refs/tags/$Tag\"\n+                if ($LASTEXITCODE -ne 0) {\n+                    throw \"git checkout tag $Tag failed (exit $LASTEXITCODE)\"\n+                }\n+            }\n+        } finally {\n+            $ErrorActionPreference = $prevEAP\n+        }\n+    }\n+\n     # Ensure submodules are initialized and updated\n     Write-Info \"Initializing submodules...\"\n     git -c windows.appendAtomically=false submodule update --init --recursive 2>$null\n"]}]},{"kind":"pr","number":28732,"title":"fix(install.ps1): remove UTF-8 BOM that breaks irm | iex path","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#28169","number":28169}],"category":"uncategorized","implication":"needs_llm"},"merged":false},{"kind":"pr","number":29206,"title":"fix(install): correct param() block placement in install.ps1 (#28141)","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"a53e8ca73327da1b4788ef4b913c59155cfbc662","number":0},{"kind":"pr","value":"#28141","number":28141},{"kind":"pr","value":"#27397","number":27397}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false}],"trust":{"default_tier":"verified","includes_suggested":false}}