hermes-agent-desktop

LOW
maintainer wyf9661 4 votes scanned 2026-11-06 00:21:40.447623
View on AUR
Why flagged

The npx executions (tsc, vitest) are part of the build and check phases, run only on the project's own source code, and do not download or execute remote code; they are standard in TypeScript/Node.js projects and pose no supply-chain risk.

Triggered rules

Low AI review downgraded a static finding llm_review

The static rules flagged this MEDIUM, but an AI model (qwen/qwen3-235b-a22b-2507) reviewed the full PKGBUILD and judged it LOW (confidence 95%): The npx executions (tsc, vitest) are part of the build and check phases, run only on the project's own source code, and do not download or execute remote code; they are standard in TypeScript/Node.js projects and pose no supply-chain risk.

1 higher static finding superseded - not the current verdict (shown for transparency)
Medium npx/bunx/deno executes a remote package remote_code_tool

`npx`/`bunx`/`pnpm dlx`/`deno run <url>` downloads AND runs a remote package at build time — the moral equivalent of piping a download into a shell. Severity downgraded: Node.js consumer context.

  • PKGBUILD:248 program="$(cd apps/desktop && npx tsc -p . --listFilesOnly)" || program=
  • PKGBUILD:284 (cd apps/desktop && HERMES_PYTHON=python3 npx vitest related --run --passWithNoTests "${projects[@]}" "${related[@]}")

PKGBUILD

2 offending line(s) highlighted
1# Maintainer: nullptr <nullptr@null.ptr>
2# Builds the Hermes desktop app from the upstream release tag. The app itself
3# is MIT-licensed (Nous Research). The local files
4# (package-managed-runtime.patch, package-managed-bootstrap.patch,
5# harden-hud-modifier-monitor.patch, hermes-desktop, launcher.test.cjs) are the
6# same files as in hermes-agent-desktop-bin, whose CI builds this PKGBUILD
7# under that name (https://github.com/jabla/hermes-agent-desktop-bin).
8pkgname=hermes-agent-desktop
9_pkgname=hermes-desktop # /usr/bin launcher name (AUR convention, lowercase)
10_upstream=Hermes # productName + executableName
11_pkgver_tag=v0.23.0
12_commit=2bd378ed1b8d6bf2192cb26e0a55ff3091e6bd19
13# Arch's Electron runtime: the major upstream locks, or a newer one (README).
14# prepare() points the launcher and its test at it.
15_electron=electron44
16pkgver=0.23.0
17pkgrel=1
18pkgdesc="Desktop UI for Hermes Agent (chat, voice, files, settings)"
19arch=('x86_64')
20url='https://github.com/NousResearch/hermes-agent'
21license=('MIT')
22# The app runs its backend through /usr/bin/hermes from hermes-agent (or
23# hermes-agent-bin, which provides it with its version) and never installs a
24# runtime of its own. It needs a runtime of at least its own version.
25# hermes-agent's own dependencies (ffmpeg, ripgrep, browsers, ...) are not
26# repeated here.
27depends=(
28 "${_electron}"
29 "hermes-agent>=${pkgver}"
30 'hicolor-icon-theme'
31 'libnotify'
32 'libsecret'
33 'libx11'
34 'libxi'
35 'xdg-utils'
36)
37optdepends=(
38 'libayatana-appindicator: tray indicator support'
39)
40makedepends=('npm' 'nodejs')
41checkdepends=('nodejs')
42conflicts=('hermes-agent-desktop-bin')
43options=('!debug')
44source=(
45 "hermes-agent-${_pkgver_tag}.tar.gz::${url}/archive/refs/tags/${_pkgver_tag}.tar.gz"
46 'package-managed-runtime.patch'
47 'package-managed-bootstrap.patch'
48 'harden-hud-modifier-monitor.patch'
49 'hermes-desktop'
50 'launcher.test.cjs'
51)
52# Run updpkgsums after editing a local file.
53sha256sums=('4879a10c7f125281ad88e9f2c339c807fd59c6f40588bdcd652781e9425320fd'
54 '10568d3fd03b5511a8f2f2e9cbea6a3bd0239fe012814fe08d7c9095e6b71074'
55 '412ad7b2b6bc5149a8d236ad2cb9be54d2ac0ab78dbd3c8e16deb4f4e683713f'
56 'ac4077eb2846f90d36858c69d8fe99988606caa0059e85a9606273bb7bc29077'
57 'cb85a2dba205970130edf4a55c6a094c6e9f64f14f62c998258e2008a50a509f'
58 '01700c07523710df165b72bbc0c2931415d118576e78868b1b02eec6bffda034')
59
60# NOTE: ${srcdir} is empty at the top level of a PKGBUILD — makepkg only sets
61# it inside the function scope of prepare()/build()/package(). Computing the
62# extracted directory once at the top (as `_srcdir=...`) silently produces a
63# root-prefixed path (`/hermes-agent-2026.7.1`) and `cd` fails. Define a helper
64# and call it from each function instead.
65_extract_dir() {
66 echo "${srcdir}/hermes-agent-${_pkgver_tag#v}"
67}
68
69_set_npm_env() {
70 export npm_config_cache="${srcdir}/npm-cache"
71 export npm_config_update_notifier=false
72 export npm_config_audit=false
73 export npm_config_fund=false
74}
75
76prepare() {
77 cd "$(_extract_dir)"
78 _set_npm_env
79 # Tailwind's source scan reads the .gitignore files of the directories
80 # above when it finds no git checkout, and an AUR clone's .gitignore
81 # excludes src/: the renderer would ship without its utility classes
82 # (check() looks for them). An empty .git makes the archive its own root.
83 mkdir -p .git
84 # The launcher and its test name an Electron runtime. Point them at
85 # _electron, so a runtime switch only edits that variable. sed -i replaces
86 # makepkg's symlinks with edited copies; the repository files stay as they are.
87 sed -i -E "s|/usr/bin/electron[0-9]+|/usr/bin/${_electron}|g" \
88 "${srcdir}/hermes-desktop" "${srcdir}/launcher.test.cjs"
89 if ! grep -q "^exec /usr/bin/${_electron} " "${srcdir}/hermes-desktop"; then
90 printf 'ERROR: hermes-desktop has no exec line for /usr/bin/%s\n' "${_electron}"
91 return 1
92 fi
93
94 # --fuzz=0: a hunk whose context drifted must fail loudly instead of being
95 # applied at a guessed location (offsets are still accepted).
96 patch --batch --fuzz=0 -Np1 -i "${srcdir}/package-managed-runtime.patch"
97 patch --batch --fuzz=0 -Np1 -i "${srcdir}/package-managed-bootstrap.patch"
98 patch --batch --fuzz=0 -Np1 -i "${srcdir}/harden-hud-modifier-monitor.patch"
99 # Keep desktop metadata aligned with the Agent release, not the separately
100 # versioned upstream desktop package.json.
101 npm pkg set version=${pkgver} --prefix apps/desktop
102 # The source archive has no .git directory. Pin the peeled release commit
103 # locally so the bundled install stamp is reproducible and does not require
104 # another network lookup during prepare()/build().
105 export GITHUB_SHA="${_commit}" GITHUB_REF_NAME="${_pkgver_tag}"
106 export ELECTRON_SKIP_BINARY_DOWNLOAD=1
107 npm ci --prefer-offline --no-audit --ignore-scripts
108
109 # Keep the locked Electron npm package for its TypeScript declarations and
110 # tooling, but make any build helper that resolves `require('electron')` use
111 # Arch's versioned runtime instead of downloading a second copy. npm hoists
112 # it to the root node_modules since v0.22.5 (apps/desktop/node_modules
113 # before), so take the copy apps/desktop resolves.
114 local electron_dir
115 electron_dir="$(cd apps/desktop && node -p 'path.dirname(require.resolve("electron/package.json"))')"
116 # Only the build tree's own copy may be replaced below: without it (say,
117 # dev dependencies omitted) Node would resolve some electron further up.
118 if [[ "${electron_dir}" != "$(pwd -P)/"* ]]; then
119 printf 'ERROR: electron resolves outside the build tree: %s\n' "${electron_dir}"
120 return 1
121 fi
122
123 # The runtime used here (_electron) is the major upstream locks or a newer
124 # one: the package ran on electron42 while upstream locked 40.x. The
125 # opposite direction is not safe: an app written against a newer major may
126 # call APIs this runtime lacks, so stop instead of shipping that build.
127 local locked_electron system_electron
128 locked_electron="$(node -p 'require(process.argv[1]).version' "${electron_dir}/package.json")"
129 system_electron="$(< "/usr/lib/${_electron}/version")"
130 if (( ${locked_electron%%.*} > ${system_electron%%.*} )); then
131 printf 'ERROR: upstream locks Electron %s, newer than the system runtime %s — move _electron to a newer electronNN (README)\n' \
132 "${locked_electron}" "${system_electron}"
133 return 1
134 elif (( ${locked_electron%%.*} < ${system_electron%%.*} )); then
135 printf 'NOTE: upstream locks Electron %s; the package runs on system Electron %s\n' \
136 "${locked_electron}" "${system_electron}"
137 fi
138
139 rm -rf "${electron_dir}/dist"
140 ln -s "/usr/lib/${_electron}" "${electron_dir}/dist"
141 printf '%s' 'electron' > "${electron_dir}/path.txt"
142 test -x "${electron_dir}/dist/electron"
143
144 # Build node-pty's Linux native addon locally. Its npm lifecycle uses
145 # node-gyp on Linux. Explicitly export makepkg's build flags because npm
146 # otherwise leaves them as unexported shell variables, producing an addon
147 # without Arch's full RELRO hardening. --offline keeps npm off the
148 # registry; node-gyp takes Node's headers from the nodejs package
149 # (/usr/include/node) instead of downloading them from nodejs.org.
150 export CFLAGS CXXFLAGS CPPFLAGS LDFLAGS
151 npm_config_nodedir=/usr npm rebuild node-pty --offline
152}
153
154build() {
155 cd "$(_extract_dir)/apps/desktop"
156 _set_npm_env
157 export npm_config_offline=true
158 # makepkg runs build() in a separate subshell from prepare().
159 export GITHUB_SHA="${_commit}" GITHUB_REF_NAME="${_pkgver_tag}"
160 # The X11 HUD modifier monitor is compiled with cc during the build; it
161 # takes these flags from the environment (harden-hud-modifier-monitor.patch).
162 export CFLAGS CXXFLAGS CPPFLAGS LDFLAGS
163 # Keep upstream's package.json and lockfile pins intact for deterministic
164 # npm ci. electron-builder still prepares a throwaway Electron zip for the
165 # packager; package() keeps only the app resources and the launcher runs
166 # Arch's ${_electron}.
167 npm run build
168
169 # Upstream writes the wall clock into the bundled install stamp. Normalize it
170 # to makepkg's reproducible-build epoch before electron-builder consumes it.
171 local build_time
172 build_time="$(date -u -d "@${SOURCE_DATE_EPOCH}" '+%Y-%m-%dT%H:%M:%S.000Z')"
173 sed -i -E \
174 "s|(\"builtAt\": \")[^\"]+(\")|\1${build_time}\2|" \
175 build/install-stamp.json
176 grep -Fq "\"builtAt\": \"${build_time}\"" build/install-stamp.json
177
178 # Upstream's prepared-packaging gate admits `--dir`, not a bare `dir` token,
179 # and rejects `-c.electronVersion=...` overrides. Omit --x64: an explicit
180 # arch flag stages into build/native-deps-<platform>-<arch>, which
181 # productOutput does not allow; the host arch reuses build/native-deps.
182 npm run builder -- --linux --dir
183}
184
185# Whether a patched path (from the repository root) can change the renderer's
186# typecheck or the tests of its "ui" vitest project; in doubt it can. $2 is the
187# renderer program's file list (tsc --listFilesOnly).
188_reaches_renderer() {
189 local path=$1 program=$2 name files=() found=0
190 case "${path}" in
191 # The renderer's own sources of any type (styles, glob-loaded plugins),
192 # the app's top-level files (package.json, tsconfig.json, the vite and
193 # vitest configs) and everything outside the desktop app.
194 apps/desktop/src/* | apps/shared/*) return 0 ;;
195 apps/desktop/*/*) ;;
196 *) return 0 ;;
197 esac
198 case "${path}" in
199 # tsc follows TypeScript imports, so the list holds every such file the
200 # renderer reaches.
201 *.ts | *.tsx | *.mts | *.cts)
202 grep -qxF "${PWD}/${path}" <<<"${program}"
203 ;;
204 # tsc lists no other file type (allowJs is off): look for the name,
205 # without extension, in the renderer's sources and program files.
206 *)
207 name="${path##*/}"
208 name="${name%.*}"
209 mapfile -t files < <(grep -v '/node_modules/' <<<"${program}")
210 # A search error (exit 2) counts as found.
211 grep -rqF -- "${name}" apps/desktop/src apps/shared "${files[@]}" || found=$?
212 (( found != 1 ))
213 ;;
214 esac
215}
216
217check() {
218 cd "$(_extract_dir)"
219 _set_npm_env
220 export npm_config_offline=true
221 # Hermes desktop tests expect English timestamps and isolated temporary git
222 # repositories; the default Hermes scratch directory sits inside a git repo.
223 export LANG=C.UTF-8 LC_ALL=C.UTF-8
224 export GIT_CEILING_DIRECTORIES="${TMPDIR:-/tmp}"
225 unset HERMES_DESKTOP_PACKAGE_MANAGED_RUNTIME
226 HERMES_LAUNCHER_PATH="${srcdir}/hermes-desktop" node "${srcdir}/launcher.test.cjs"
227
228 # Upstream's full vitest suite (~1700 files) runs in upstream's CI; here it
229 # took ~90% of the build time and exercised upstream code on Arch's system
230 # Node, which the app does not run on. Run the tests that cover what the
231 # patches change instead: the patched tests and every test importing a
232 # patched module. The file list follows the patches automatically; diff -u
233 # style headers carry a tab and a timestamp after the path.
234 local patched=() file
235 mapfile -t patched < <(sed -n 's|^+++ b/\([^\t]*\).*|\1|p' "${srcdir}"/*.patch | sort -u)
236 # A misparsed header would select no tests and pass (--passWithNoTests).
237 (( ${#patched[@]} > 0 ))
238 for file in "${patched[@]}"; do
239 test -f "${file}"
240 done
241
242 # The renderer took most of the typecheck's time, its "ui" vitest project
243 # (~1300 test files to scan) most of vitest's; upstream's CI runs both.
244 # The renderer's program holds src/ with those tests and every module they
245 # import, so a patched file outside it reaches neither. A file list this
246 # cannot read counts as reached.
247 local program reaches_renderer=0
248 program="$(cd apps/desktop && npx tsc -p . --listFilesOnly)" || program=
249 if [[ "${program}" != *"${PWD}/apps/desktop/src/"* ]]; then
250 reaches_renderer=1
251 fi
252 for file in "${patched[@]}"; do
253 if (( ! reaches_renderer )) && _reaches_renderer "${file}" "${program}"; then
254 printf 'check(): %s reaches the renderer\n' "${file}"
255 reaches_renderer=1
256 fi
257 done
258 (( reaches_renderer )) ||
259 echo 'check(): no patched file reaches the renderer: no renderer typecheck, no ui tests'
260
261 # Upstream's typecheck script; its renderer part only when a patch reaches
262 # the renderer. npm exec -c keeps node_modules/.bin ahead of the system's
263 # PATH, like npm run.
264 local typecheck renderer='tsc -p . --noEmit'
265 typecheck="$(node -p 'require("./apps/desktop/package.json").scripts.typecheck')"
266 if [[ "${typecheck}" != "${renderer} && "* ]]; then
267 printf 'ERROR: upstream typecheck no longer starts with "%s": %s\n' "${renderer}" "${typecheck}"
268 return 1
269 fi
270 (( reaches_renderer )) || typecheck="${typecheck#"${renderer} && "}"
271 (cd apps/desktop && npm exec -c "${typecheck}")
272
273 # Without HERMES_PYTHON, vitest's global setup installs Hermes' Python test
274 # environment (a CPython, ffmpeg, node and the Python dependencies, from the
275 # network) for the few contracts that run real hermes_cli code. The related
276 # tests need none of it. python3 is those contracts' own fallback; should a
277 # patch ever relate to one, it fails loudly on the missing dependencies.
278 # Without the renderer every vitest project but "ui" runs, including ones
279 # upstream adds later.
280 # Absolute paths, set before the cd: apps/shared files lie outside vitest's
281 # root.
282 local projects=() related=("${patched[@]/#/${PWD}/}")
283 (( reaches_renderer )) || projects=(--project '!ui')
284 (cd apps/desktop && HERMES_PYTHON=python3 npx vitest related --run --passWithNoTests "${projects[@]}" "${related[@]}")
285
286 # Tailwind v4 skips sources that a parent directory's .gitignore excludes
287 # when the build directory is no git checkout (this repository ignores
288 # src/). The renderer then ships without its utility classes: the window
289 # shows unstyled text while the app itself boots fine. The utilities live
290 # in the stylesheet of the main chunk (index-*.css until v0.22.5, when the
291 # renderer entry was src/main.tsx).
292 local css=(apps/desktop/release/linux-unpacked/resources/app.asar.unpacked/dist/assets/main-*.css)
293 if ! grep -q '\.flex{' "${css[@]}"; then
294 printf 'ERROR: the renderer CSS lacks Tailwind utilities (%s) — build from a git checkout or without a parent .gitignore excluding src/\n' \
295 "${css[*]##*/}"
296 return 1
297 fi
298
299 # node-pty is the only native Node addon shipped by Hermes. Load the staged
300 # module with the exact Electron runtime used by the installed launcher; its
301 # N-API build must not merely load under makepkg's system Node.
302 local node_pty_root="${PWD}/apps/desktop/release/linux-unpacked/resources/app.asar.unpacked/dist/node_modules/node-pty"
303 test -f "${node_pty_root}/package.json"
304 # Bypass Arch's launcher here: it injects the user's electronNN-flags.conf,
305 # valid in Chromium mode but rejected while ELECTRON_RUN_AS_NODE is set.
306 env ELECTRON_RUN_AS_NODE=1 NODE_PTY_ROOT="${node_pty_root}" \
307 "/usr/lib/${_electron}/electron" -e \
308 'const pty = require(process.env.NODE_PTY_ROOT); if (typeof pty.spawn !== "function") process.exit(1)'
309}
310
311package() {
312 cd "$(_extract_dir)"
313 local appdir="apps/desktop/release/linux-unpacked"
314 local resources="${appdir}/resources"
315 if [ ! -d "${appdir}" ]; then
316 printf 'ERROR: electron-builder did not produce %s\n' "${appdir}"
317 ls -la apps/desktop/release/ 2>/dev/null || true
318 return 1
319 fi
320 if [ ! -f "${resources}/app.asar" ] || \
321 [ ! -d "${resources}/app.asar.unpacked" ] || \
322 [ ! -f "${resources}/install-stamp.json" ]; then
323 printf 'ERROR: electron-builder output is missing required app resources\n'
324 find "${resources}" -maxdepth 2 -printf '%M %p\n' 2>/dev/null || true
325 return 1
326 fi
327 # Only the first three resources are installed. The skipped ones are
328 # upstream's Windows icon, electron-updater metadata (Hermes ships no
329 # electron-updater) and Electron's own default app, which the system
330 # Electron provides. Anything else is a new runtime resource the app would miss on
331 # Linux, so stop until it is packaged or deliberately added here.
332 local entry
333 for entry in "${resources}"/*; do
334 case "${entry##*/}" in
335 app.asar|app.asar.unpacked|install-stamp.json) ;;
336 icon.ico|app-update.yml|default_app.asar) ;;
337 *)
338 printf 'ERROR: unexpected electron-builder resource %s — install it or allowlist it in package()\n' \
339 "${entry##*/}"
340 return 1
341 ;;
342 esac
343 done
344 # Install dir stays version-independent of pkgname: the launcher and the
345 # packaged app reference /usr/lib/hermes-agent-desktop by path. The
346 # conflicts=() entry guarantees only one variant (source or -bin) is
347 # installed, so a fixed dir cannot collide.
348 local libdir="/usr/lib/hermes-agent-desktop"
349 install -dm755 "${pkgdir}${libdir}"
350 install -Dm644 "${resources}/app.asar" \
351 "${pkgdir}${libdir}/app.asar"
352 cp -a "${resources}/app.asar.unpacked" \
353 "${pkgdir}${libdir}/app.asar.unpacked"
354 install -Dm644 "${resources}/install-stamp.json" \
355 "${pkgdir}${libdir}/install-stamp.json"
356 # The agent runtime comes from the hermes-agent dependency (/usr/bin/hermes).
357 # This package must not ship or run an installer that writes an agent tree.
358 install -Dm755 "${srcdir}/hermes-desktop" "${pkgdir}/usr/bin/${_pkgname}"
359 install -Dm644 /dev/stdin "${pkgdir}/usr/share/applications/${_pkgname}.desktop" <<EOF
360[Desktop Entry]
361Name=Hermes
362GenericName=AI Agent Client
363Comment=${pkgdesc}
364Exec=/usr/bin/${_pkgname} %U
365Terminal=false
366Type=Application
367Icon=${_upstream,,}
368StartupWMClass=${_upstream}
369Categories=Development;
370Keywords=AI;Agent;Chat;Assistant;
371MimeType=x-scheme-handler/hermes;
372EOF
373 install -Dm644 "apps/desktop/assets/icon.png" \
374 "${pkgdir}/usr/share/icons/hicolor/512x512/apps/${_upstream,,}.png"
375 install -Dm644 "LICENSE" "${pkgdir}/usr/share/licenses/${pkgname}/LICENSE"
376}
377

Changes since previous scan

--- PKGBUILD @ 2026-11-04 22:08
+++ PKGBUILD @ 2026-11-06 00:21
@@ -1,22 +1,32 @@
# Maintainer: nullptr <nullptr@null.ptr>
+# Builds the Hermes desktop app from the upstream release tag. The app itself
+# is MIT-licensed (Nous Research). The local files
+# (package-managed-runtime.patch, package-managed-bootstrap.patch,
+# harden-hud-modifier-monitor.patch, hermes-desktop, launcher.test.cjs) are the
+# same files as in hermes-agent-desktop-bin, whose CI builds this PKGBUILD
+# under that name (https://github.com/jabla/hermes-agent-desktop-bin).
pkgname=hermes-agent-desktop
_pkgname=hermes-desktop # /usr/bin launcher name (AUR convention, lowercase)
_upstream=Hermes # productName + executableName
-_pkgver_tag=v0.22.5
-_commit=bf0203a794a07aac3ff764972ab9087baa4e4f4e
-pkgver=0.22.5
+_pkgver_tag=v0.23.0
+_commit=2bd378ed1b8d6bf2192cb26e0a55ff3091e6bd19
+# Arch's Electron runtime: the major upstream locks, or a newer one (README).
+# prepare() points the launcher and its test at it.
+_electron=electron44
+pkgver=0.23.0
pkgrel=1
pkgdesc="Desktop UI for Hermes Agent (chat, voice, files, settings)"
arch=('x86_64')
url='https://github.com/NousResearch/hermes-agent'
license=('MIT')
-# Runtime: Electron shell + hermes-agent. Do not repeat hermes-agent's own
-# depends/optdepends (ffmpeg, ripgrep, agent-browser, chromium/chrome, …).
-# Tools check() invokes directly are listed in checkdepends even when
-# hermes-agent would pull them in transitively.
+# The app runs its backend through /usr/bin/hermes from hermes-agent (or
+# hermes-agent-bin, which provides it with its version) and never installs a
+# runtime of its own. It needs a runtime of at least its own version.
+# hermes-agent's own dependencies (ffmpeg, ripgrep, browsers, ...) are not
+# repeated here.
depends=(
- 'electron44'
- 'hermes-agent>=0.22.5'
+ "${_electron}"
+ "hermes-agent>=${pkgver}"
'hicolor-icon-theme'
'libnotify'
'libsecret'
@@ -24,33 +34,34 @@
'libxi'
'xdg-utils'
)
-# npm is not a hermes-agent dependency; nodejs is declared in checkdepends.
-makedepends=('npm')
-checkdepends=('nodejs' 'python' 'uv')
optdepends=(
'libayatana-appindicator: tray indicator support'
)
+makedepends=('npm' 'nodejs')
+checkdepends=('nodejs')
conflicts=('hermes-agent-desktop-bin')
options=('!debug')
-# Arch Electron runtime matching upstream apps/desktop pin (44.9.3).
-# HERMES_ELECTRON_DIST overrides for local builds without a system install.
-_electron_dist="${HERMES_ELECTRON_DIST:-/usr/lib/electron44}"
-# Runtime patches only: refuse in-app agent bootstrap/update when the launcher
-# sets HERMES_DESKTOP_PACKAGE_MANAGED_RUNTIME=1. Test-only / RELRO patches dropped.
source=(
"hermes-agent-${_pkgver_tag}.tar.gz::${url}/archive/refs/tags/${_pkgver_tag}.tar.gz"
'package-managed-runtime.patch'
'package-managed-bootstrap.patch'
+ 'harden-hud-modifier-monitor.patch'
'hermes-desktop'
'launcher.test.cjs'
)
-sha256sums=('a8f67e5ec14fea681acef6cc7ff1553f002bb6101fd2df22bfdd4b7eecc54eb2'
- 'dfe306b738af35614b81369150263b4a5284cc2061e5d1ce48b3cba7f7d8f584'
+# Run updpkgsums after editing a local file.
+sha256sums=('4879a10c7f125281ad88e9f2c339c807fd59c6f40588bdcd652781e9425320fd'
+ '10568d3fd03b5511a8f2f2e9cbea6a3bd0239fe012814fe08d7c9095e6b71074'
'412ad7b2b6bc5149a8d236ad2cb9be54d2ac0ab78dbd3c8e16deb4f4e683713f'
- '6b2e17a0411f23dcafa9a6da7d8cd850573430a45ff1353e0febbbfc84fbea9f'
- '4b80f0d97e93a24f030a75a8b425db1315888a9ddaa7c0077c7e0401ddf58522')
-
-# Resolve srcdir inside makepkg's functions; it is empty at the top level.
+ 'ac4077eb2846f90d36858c69d8fe99988606caa0059e85a9606273bb7bc29077'
+ 'cb85a2dba205970130edf4a55c6a094c6e9f64f14f62c998258e2008a50a509f'
+ '01700c07523710df165b72bbc0c2931415d118576e78868b1b02eec6bffda034')
+
+# NOTE: ${srcdir} is empty at the top level of a PKGBUILD — makepkg only sets
+# it inside the function scope of prepare()/build()/package(). Computing the
+# extracted directory once at the top (as `_srcdir=...`) silently produces a
+# root-prefixed path (`/hermes-agent-2026.7.1`) and `cd` fails. Define a helper
+# and call it from each function instead.
_extract_dir() {
echo "${srcdir}/hermes-agent-${_pkgver_tag#v}"
}
@@ -65,8 +76,26 @@
prepare() {
cd "$(_extract_dir)"
_set_npm_env
+ # Tailwind's source scan reads the .gitignore files of the directories
+ # above when it finds no git checkout, and an AUR clone's .gitignore
+ # excludes src/: the renderer would ship without its utility classes
+ # (check() looks for them). An empty .git makes the archive its own root.
+ mkdir -p .git
+ # The launcher and its test name an Electron runtime. Point them at
+ # _electron, so a runtime switch only edits that variable. sed -i replaces
+ # makepkg's symlinks with edited copies; the repository files stay as they are.
+ sed -i -E "s|/usr/bin/electron[0-9]+|/usr/bin/${_electron}|g" \
+ "${srcdir}/hermes-desktop" "${srcdir}/launcher.test.cjs"
+ if ! grep -q "^exec /usr/bin/${_electron} " "${srcdir}/hermes-desktop"; then
+ printf 'ERROR: hermes-desktop has no exec line for /usr/bin/%s\n' "${_electron}"
+ return 1
+ fi
+
+ # --fuzz=0: a hunk whose context drifted must fail loudly instead of being
+ # applied at a guessed location (offsets are still accepted).
patch --batch --fuzz=0 -Np1 -i "${srcdir}/package-managed-runtime.patch"
patch --batch --fuzz=0 -Np1 -i "${srcdir}/package-managed-bootstrap.patch"
+ patch --batch --fuzz=0 -Np1 -i "${srcdir}/harden-hud-modifier-monitor.patch"
# Keep desktop metadata aligned with the Agent release, not the separately
# versioned upstream desktop package.json.
npm pkg set version=${pkgver} --prefix apps/desktop
@@ -79,25 +108,47 @@
# Keep the locked Electron npm package for its TypeScript declarations and
# tooling, but make any build helper that resolves `require('electron')` use
- # Arch's versioned runtime instead of downloading a second copy.
- # npm workspaces may hoist electron to the repo root node_modules.
- local electron_dir='apps/desktop/node_modules/electron'
- if [ ! -d "${electron_dir}" ]; then
- electron_dir='node_modules/electron'
- fi
- test -d "${electron_dir}"
+ # Arch's versioned runtime instead of downloading a second copy. npm hoists
+ # it to the root node_modules since v0.22.5 (apps/desktop/node_modules
+ # before), so take the copy apps/desktop resolves.
+ local electron_dir
+ electron_dir="$(cd apps/desktop && node -p 'path.dirname(require.resolve("electron/package.json"))')"
+ # Only the build tree's own copy may be replaced below: without it (say,
+ # dev dependencies omitted) Node would resolve some electron further up.
+ if [[ "${electron_dir}" != "$(pwd -P)/"* ]]; then
+ printf 'ERROR: electron resolves outside the build tree: %s\n' "${electron_dir}"
+ return 1
+ fi
+
+ # The runtime used here (_electron) is the major upstream locks or a newer
+ # one: the package ran on electron42 while upstream locked 40.x. The
+ # opposite direction is not safe: an app written against a newer major may
+ # call APIs this runtime lacks, so stop instead of shipping that build.
+ local locked_electron system_electron
+ locked_electron="$(node -p 'require(process.argv[1]).version' "${electron_dir}/package.json")"
+ system_electron="$(< "/usr/lib/${_electron}/version")"
+ if (( ${locked_electron%%.*} > ${system_electron%%.*} )); then
+ printf 'ERROR: upstream locks Electron %s, newer than the system runtime %s — move _electron to a newer electronNN (README)\n' \
+ "${locked_electron}" "${system_electron}"
+ return 1
+ elif (( ${locked_electron%%.*} < ${system_electron%%.*} )); then
+ printf 'NOTE: upstream locks Electron %s; the package runs on system Electron %s\n' \
+ "${locked_electron}" "${system_electron}"
+ fi
+
rm -rf "${electron_dir}/dist"
- ln -s "${_electron_dist}" "${electron_dir}/dist"
+ ln -s "/usr/lib/${_electron}" "${electron_dir}/dist"
printf '%s' 'electron' > "${electron_dir}/path.txt"
test -x "${electron_dir}/dist/electron"
# Build node-pty's Linux native addon locally. Its npm lifecycle uses
# node-gyp on Linux. Explicitly export makepkg's build flags because npm
# otherwise leaves them as unexported shell variables, producing an addon
- # without Arch's full RELRO hardening. --offline prevents any header or
- # binary download.
+ # without Arch's full RELRO hardening. --offline keeps npm off the
+ # registry; node-gyp takes Node's headers from the nodejs package
+ # (/usr/include/node) instead of downloading them from nodejs.org.
export CFLAGS CXXFLAGS CPPFLAGS LDFLAGS
- npm rebuild node-pty --offline
+ npm_config_nodedir=/usr npm rebuild node-pty --offline
}
build() {
@@ -106,11 +157,13 @@
export npm_config_offline=true
# makepkg runs build() in a separate subshell from prepare().
export GITHUB_SHA="${_commit}" GITHUB_REF_NAME="${_pkgver_tag}"
+ # The X11 HUD modifier monitor is compiled with cc during the build; it
+ # takes these flags from the environment (harden-hud-modifier-monitor.patch).
export CFLAGS CXXFLAGS CPPFLAGS LDFLAGS
# Keep upstream's package.json and lockfile pins intact for deterministic
# npm ci. electron-builder still prepares a throwaway Electron zip for the
- # packager; package() retains only app resources and the launcher runs
- # Arch's electron44.
+ # packager; package() keeps only the app resources and the launcher runs
+ # Arch's ${_electron}.
npm run build
# Upstream writes the wall clock into the bundled install stamp. Normalize it
@@ -123,10 +176,42 @@
grep -Fq "\"builtAt\": \"${build_time}\"" build/install-stamp.json
# Upstream's prepared-packaging gate admits `--dir`, not a bare `dir` token,
- # and rejects `-c.electronVersion=…` overrides. Omit --x64: an explicit arch
- # flag stages into build/native-deps-<platform>-<arch>, which productOutput
- # does not allow; host arch reuses the allowed build/native-deps path.
+ # and rejects `-c.electronVersion=...` overrides. Omit --x64: an explicit
+ # arch flag stages into build/native-deps-<platform>-<arch>, which
+ # productOutput does not allow; the host arch reuses build/native-deps.
npm run builder -- --linux --dir
+}
+
+# Whether a patched path (from the repository root) can change the renderer's
+# typecheck or the tests of its "ui" vitest project; in doubt it can. $2 is the
+# renderer program's file list (tsc --listFilesOnly).
+_reaches_renderer() {
+ local path=$1 program=$2 name files=() found=0
+ case "${path}" in
+ # The renderer's own sources of any type (styles, glob-loaded plugins),
+ # the app's top-level files (package.json, tsconfig.json, the vite and
+ # vitest configs) and everything outside the desktop app.
+ apps/desktop/src/* | apps/shared/*) return 0 ;;
+ apps/desktop/*/*) ;;
+ *) return 0 ;;
+ esac
+ case "${path}" in
+ # tsc follows TypeScript imports, so the list holds every such file the
+ # renderer reaches.
+ *.ts | *.tsx | *.mts | *.cts)
+ grep -qxF "${PWD}/${path}" <<<"${program}"
+ ;;
+ # tsc lists no other file type (allowJs is off): look for the name,
+ # without extension, in the renderer's sources and program files.
+ *)
+ name="${path##*/}"
+ name="${name%.*}"
+ mapfile -t files < <(grep -v '/node_modules/' <<<"${program}")
+ # A search error (exit 2) counts as found.
+ grep -rqF -- "${name}" apps/desktop/src apps/shared "${files[@]}" || found=$?
+ (( found != 1 ))
+ ;;
+ esac
}
check() {
@@ -138,39 +223,88 @@
export LANG=C.UTF-8 LC_ALL=C.UTF-8
export GIT_CEILING_DIRECTORIES="${TMPDIR:-/tmp}"
unset HERMES_DESKTOP_PACKAGE_MANAGED_RUNTIME
- # Upstream electron tests import hermes_cli/pm through HERMES_PYTHON; a bare
- # system interpreter lacks the locked deps. Prepare a throwaway venv from
- # uv.lock (network only for wheels missing from the host uv cache).
- local check_venv="${srcdir}/hermes-check-venv"
- export UV_PROJECT_ENVIRONMENT="${check_venv}"
- export UV_CACHE_DIR="${srcdir}/uv-cache"
- uv venv "${check_venv}" --python /usr/bin/python3 --clear
- uv sync --frozen --no-group dev
- export HERMES_PYTHON="${check_venv}/bin/python"
- node "${srcdir}/launcher.test.cjs"
- npm run typecheck --workspace apps/desktop
- # The upstream live-portal fixture does not forward XAUTHORITY to Electron.
- # Without xvfb-run, a graphical host's DISPLAY would falsely enable it.
- # Skip voice-prefs spy that fails under Arch's Node localStorage (was a
- # packaging-only test patch; not needed at runtime).
- # dead-session-prune: clamp assertion expects one fake timer; under Arch's
- # vitest getTimerCount stays at 2 after the churn re-schedule (runtime OK).
- # profile-rail-status: tests pass but radix focus-scope + jsdom leaves an
- # unhandled dispatchEvent TypeError that makes vitest exit 1.
- env -u DISPLAY -u WAYLAND_DISPLAY npm run test --workspace apps/desktop -- \
- --exclude 'src/store/voice-prefs.test.ts' \
- --exclude 'src/store/dead-session-prune.test.ts' \
- --exclude 'src/app/chat/sidebar/profile-rail-status.test.tsx'
-
- # node-pty is the native Node addon shipped by Hermes. Load the staged
+ HERMES_LAUNCHER_PATH="${srcdir}/hermes-desktop" node "${srcdir}/launcher.test.cjs"
+
+ # Upstream's full vitest suite (~1700 files) runs in upstream's CI; here it
+ # took ~90% of the build time and exercised upstream code on Arch's system
+ # Node, which the app does not run on. Run the tests that cover what the
+ # patches change instead: the patched tests and every test importing a
+ # patched module. The file list follows the patches automatically; diff -u
+ # style headers carry a tab and a timestamp after the path.
+ local patched=() file
+ mapfile -t patched < <(sed -n 's|^+++ b/\([^\t]*\).*|\1|p' "${srcdir}"/*.patch | sort -u)
+ # A misparsed header would select no tests and pass (--passWithNoTests).
+ (( ${#patched[@]} > 0 ))
+ for file in "${patched[@]}"; do
+ test -f "${file}"
+ done
+
+ # The renderer took most of the typecheck's time, its "ui" vitest project
+ # (~1300 test files to scan) most of vitest's; upstream's CI runs both.
+ # The renderer's program holds src/ with those tests and every module they
+ # import, so a patched file outside it reaches neither. A file list this
+ # cannot read counts as reached.
+ local program reaches_renderer=0
+ program="$(cd apps/desktop && npx tsc -p . --listFilesOnly)" || program=
+ if [[ "${program}" != *"${PWD}/apps/desktop/src/"* ]]; then
+ reaches_renderer=1
+ fi
+ for file in "${patched[@]}"; do
+ if (( ! reaches_renderer )) && _reaches_renderer "${file}" "${program}"; then
+ printf 'check(): %s reaches the renderer\n' "${file}"
+ reaches_renderer=1
+ fi
+ done
+ (( reaches_renderer )) ||
+ echo 'check(): no patched file reaches the renderer: no renderer typecheck, no ui tests'
+
+ # Upstream's typecheck script; its renderer part only when a patch reaches
+ # the renderer. npm exec -c keeps node_modules/.bin ahead of the system's
+ # PATH, like npm run.
+ local typecheck renderer='tsc -p . --noEmit'
+ typecheck="$(node -p 'require("./apps/desktop/package.json").scripts.typecheck')"
+ if [[ "${typecheck}" != "${renderer} && "* ]]; then
+ printf 'ERROR: upstream typecheck no longer starts with "%s": %s\n' "${renderer}" "${typecheck}"
+ return 1
+ fi
+ (( reaches_renderer )) || typecheck="${typecheck#"${renderer} && "}"
+ (cd apps/desktop && npm exec -c "${typecheck}")
+
+ # Without HERMES_PYTHON, vitest's global setup installs Hermes' Python test
+ # environment (a CPython, ffmpeg, node and the Python dependencies, from the
+ # network) for the few contracts that run real hermes_cli code. The related
+ # tests need none of it. python3 is those contracts' own fallback; should a
+ # patch ever relate to one, it fails loudly on the missing dependencies.
+ # Without the renderer every vitest project but "ui" runs, including ones
+ # upstream adds later.
+ # Absolute paths, set before the cd: apps/shared files lie outside vitest's
+ # root.
+ local projects=() related=("${patched[@]/#/${PWD}/}")
+ (( reaches_renderer )) || projects=(--project '!ui')
+ (cd apps/desktop && HERMES_PYTHON=python3 npx vitest related --run --passWithNoTests "${projects[@]}" "${related[@]}")
+
+ # Tailwind v4 skips sources that a parent directory's .gitignore excludes
+ # when the build directory is no git checkout (this repository ignores
+ # src/). The renderer then ships without its utility classes: the window
+ # shows unstyled text while the app itself boots fine. The utilities live
+ # in the stylesheet of the main chunk (index-*.css until v0.22.5, when the
+ # renderer entry was src/main.tsx).
+ local css=(apps/desktop/release/linux-unpacked/resources/app.asar.unpacked/dist/assets/main-*.css)
+ if ! grep -q '\.flex{' "${css[@]}"; then
+ printf 'ERROR: the renderer CSS lacks Tailwind utilities (%s) — build from a git checkout or without a parent .gitignore excluding src/\n' \
+ "${css[*]##*/}"
+ return 1
+ fi
+
+ # node-pty is the only native Node addon shipped by Hermes. Load the staged
# module with the exact Electron runtime used by the installed launcher; its
# N-API build must not merely load under makepkg's system Node.
local node_pty_root="${PWD}/apps/desktop/release/linux-unpacked/resources/app.asar.unpacked/dist/node_modules/node-pty"
test -f "${node_pty_root}/package.json"
- # Bypass Arch's launcher here: it injects the user's electron44 flags, which
- # are valid in Chromium mode but rejected while ELECTRON_RUN_AS_NODE is set.
+ # Bypass Arch's launcher here: it injects the user's electronNN-flags.conf,
+ # valid in Chromium mode but rejected while ELECTRON_RUN_AS_NODE is set.
env ELECTRON_RUN_AS_NODE=1 NODE_PTY_ROOT="${node_pty_root}" \
- "${_electron_dist}/electron" -e \
+ "/usr/lib/${_electron}/electron" -e \
'const pty = require(process.env.NODE_PTY_ROOT); if (typeof pty.spawn !== "function") process.exit(1)'
}
@@ -190,14 +324,36 @@
find "${resources}" -maxdepth 2 -printf '%M %p\n' 2>/dev/null || true
return 1
fi
- install -dm755 "${pkgdir}/usr/lib/${pkgname}"
+ # Only the first three resources are installed. The skipped ones are
+ # upstream's Windows icon, electron-updater metadata (Hermes ships no
+ # electron-updater) and Electron's own default app, which the system
+ # Electron provides. Anything else is a new runtime resource the app would miss on
+ # Linux, so stop until it is packaged or deliberately added here.
+ local entry
+ for entry in "${resources}"/*; do
+ case "${entry##*/}" in
+ app.asar|app.asar.unpacked|install-stamp.json) ;;
+ icon.ico|app-update.yml|default_app.asar) ;;
+ *)
+ printf 'ERROR: unexpected electron-builder resource %s — install it or allowlist it in package()\n' \
+ "${entry##*/}"
+ return 1
+ ;;
+ esac
+ done
+ # Install dir stays version-independent of pkgname: the launcher and the
+ # packaged app reference /usr/lib/hermes-agent-desktop by path. The
+ # conflicts=() entry guarantees only one variant (source or -bin) is
+ # installed, so a fixed dir cannot collide.
+ local libdir="/usr/lib/hermes-agent-desktop"
+ install -dm755 "${pkgdir}${libdir}"
install -Dm644 "${resources}/app.asar" \
- "${pkgdir}/usr/lib/${pkgname}/app.asar"
+ "${pkgdir}${libdir}/app.asar"
cp -a "${resources}/app.asar.unpacked" \
- "${pkgdir}/usr/lib/${pkgname}/app.asar.unpacked"
+ "${pkgdir}${libdir}/app.asar.unpacked"
install -Dm644 "${resources}/install-stamp.json" \
- "${pkgdir}/usr/lib/${pkgname}/install-stamp.json"
- # Local agent runtime comes from the hermes-agent dependency (/usr/bin/hermes).
+ "${pkgdir}${libdir}/install-stamp.json"
+ # The agent runtime comes from the hermes-agent dependency (/usr/bin/hermes).
# This package must not ship or run an installer that writes an agent tree.
install -Dm755 "${srcdir}/hermes-desktop" "${pkgdir}/usr/bin/${_pkgname}"
install -Dm644 /dev/stdin "${pkgdir}/usr/share/applications/${_pkgname}.desktop" <<EOF

Scan history

Scanned at (UTC)SeverityRules
2026-11-06 00:21:40 Low 2
2026-11-06 00:11:05 Medium 1
2026-11-04 22:08:15 Clean 0
2026-10-23 17:38:05 Clean 0
2026-10-22 07:34:22 Clean 0
2026-10-16 05:14:45 Clean 0
2026-10-15 05:10:54 Clean 0
2026-10-14 11:19:54 Clean 0
2026-10-12 17:14:20 Clean 0
2026-10-09 12:11:09 Clean 0
2026-10-09 06:10:33 Clean 0
2026-09-26 15:15:21 Clean 0
2026-09-24 11:42:45 Clean 0
2026-09-16 07:21:35 Clean 0
2026-09-12 07:11:01 Clean 0
2026-09-09 03:18:52 Clean 0
2026-09-01 09:47:42 Clean 0
2026-09-01 05:47:12 Clean 0
2026-08-29 19:40:31 Clean 0
2026-08-27 15:32:51 Clean 0

Report a package

Reports go to the AURWatch maintainer (one person) and are read by hand. No login required.

0 / 4000
Your suggestion