paseo
The npx usage in build() runs node-gyp rebuild and expo export, which are legitimate build tools acting on the project's own source; no remote code execution or untrusted package installation occurs.
Triggered rules
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 usage in build() runs node-gyp rebuild and expo export, which are legitimate build tools acting on the project's own source; no remote code execution or untrusted package installation occurs.
1 higher static finding superseded - not the current verdict (shown for transparency)
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:118
(cd "$(_node_pty_dir)" && npx node-gyp rebuild) -
PKGBUILD:132
(cd packages/app && PASEO_WEB_PLATFORM=electron npx expo export --platform web)
PKGBUILD
2 offending line(s) highlighted# Maintainer: jinzhongjia <mail@nvimer.org>
pkgname=paseo
pkgver=0.13.2
pkgrel=1
pkgdesc="One interface for all your Claude Code, Codex and OpenCode agents (built from source, runs on system Electron)"
arch=('x86_64')
url="https://paseo.sh"
_github_url="https://github.com/getpaseo/paseo"
# Keep in sync with packages/desktop/package.json devDependencies.electron —
# prepare() fails the build when the majors drift apart.
_electron_pkg=electron44
license=('Apache-2.0')
depends=("${_electron_pkg}" 'gcc-libs' 'glibc' 'hicolor-icon-theme' 'nodejs>=22')
makedepends=('npm' 'python' 'git')
optdepends=('git: agent worktree management')
conflicts=('paseo-bin' 'paseo-desktop-bin' 'paseo-appimage')
options=('!debug')
install=paseo.install
source=(
"${pkgname}-${pkgver}.tar.gz::${_github_url}/archive/refs/tags/v${pkgver}.tar.gz"
'paseo.sh'
'paseo.desktop'
'paseo.service'
'paseo-daemon-session.sh'
'trace-desktop.mjs'
'system-electron-paths.patch'
)
sha256sums=('07023ced1de45a32db63a50b8989e86217c09750d5fe7cacba20ca80dfec1d8a'
'f9e194a879a87d87021ad06f489a9e4197ded629055a38e2da3557a423785de4'
'8219842d8f7bb20e0608450f2574fa37d98e37ef410de459e0792651a78c1b47'
'df0d01b98ac405c5c25edbb91d61bb9e05355a57e0e652e00823d6331618d686'
'620279e619a4f42dcfb45991d679bc82e891d69cabc77a9512b77a47f4df59c2'
'9ed8aeb2692ae8c51e9fb72f9b099e09358159bec5f7d3bdae1fbdf5940382f2'
'4111231339f8fcad31ec5fc139f9292c26182785838f22076d4c05f8c8463579')
# Repo-relative path of the installed node-pty. npm hoists it to the root
# node_modules in some releases and nests it under packages/server in others
# (0.3.1 nests it), so resolve it instead of hardcoding either location.
_node_pty_dir() {
node -p "const p=require('node:path');p.relative(process.cwd(),p.dirname(require.resolve('node-pty/package.json',{paths:['packages/server']})))"
}
prepare() {
cd "${pkgname}-${pkgver}"
# Refuse to build against the wrong Electron major: the app would compile
# fine but hit runtime API drift. Bump _electron_pkg= (and pkgrel) when
# upstream moves on.
local _upstream_electron
_upstream_electron="$(node -p "require('./packages/desktop/package.json').devDependencies.electron.match(/\d+/)[0]")"
if [[ "electron${_upstream_electron}" != "${_electron_pkg}" ]]; then
printf 'ERROR: upstream now pins electron %s but this package uses %s; update _electron_pkg= in the PKGBUILD\n' \
"${_upstream_electron}" "${_electron_pkg}" >&2
return 1
fi
# Arch's system electron reports app.isPackaged=true, so the app takes
# its packaged code paths; point the electron-builder resource lookups
# (process.resourcesPath / app.asar*) at /usr/lib/paseo instead, and
# leave updates to pacman rather than electron-updater.
#
# The patch used to carry a sixth hunk redirecting getBundledSkillsDir()
# in packages/desktop/src/integrations/skills/paths.ts. 0.5.1 deleted that
# file: skills resolution moved to packages/server's
# orchestration-skills/internal/paths.ts, which resolves relative to its
# own compiled location and takes the first candidate that exists instead
# of reading process.resourcesPath. Its first candidate lands on
# packages/server/dist/server/skills, which the trace already installs
# (byte-identical to the top-level skills/ that package() checks for), so
# nothing needs redirecting and the hunk was dropped rather than ported.
#
# A hunk for features/editor-targets/runtime.ts was added instead:
# iconPath() reads the editor-target icons from process.resourcesPath, and
# listEditorTargets() awaits describe() -> loadIcon() for every installed
# editor with no try/catch anywhere in the chain. Under system Electron
# that path resolves into electron*/resources, so the read threw ENOENT and
# the whole editor-target list failed for anyone with a supported editor
# installed — not just a missing icon. Point it at the assets we ship.
#
# 0.11.0 folded getBundledCliShimPath()'s per-platform branches into
# resolveCliShimPath() (cli-install/path.ts), which for packaged Linux
# still resolves <dir of exe>/resources/bin/paseo — i.e. inside the system
# electron* tree. The cli-install/paths.ts hunk now short-circuits Linux to
# /usr/bin/paseo before that call, same intent as before.
patch -Np1 -i "${srcdir}/system-electron-paths.patch"
# Keep npm state inside $srcdir; skip lifecycle scripts (no electron /
# onnxruntime binary downloads — mirrors upstream nix/desktop-package.nix).
export npm_config_cache="${srcdir}/npm-cache"
export npm_config_update_notifier=false
# Survive transient registry ECONNRESETs (npm defaults to only 2 retries).
export npm_config_fetch_retries=5
export npm_config_fetch_retry_mintimeout=20000
export npm_config_fetch_retry_maxtimeout=120000
npm ci --ignore-scripts --no-audit --no-fund
# Root postinstall applies patches/ via patch-package.
npm run postinstall
}
build() {
cd "${pkgname}-${pkgver}"
export npm_config_cache="${srcdir}/npm-cache"
export npm_config_update_notifier=false
# build() still hits the network (node-gyp headers, expo); same retry bump.
export npm_config_fetch_retries=5
export npm_config_fetch_retry_mintimeout=20000
export npm_config_fetch_retry_maxtimeout=120000
export EXPO_NO_TELEMETRY=1
export CI=1
# node-pty is the only native addon the daemon needs (N-API, so it stays
# ABI-compatible with the Electron runtime). Compiled directly with
# node-gyp: `npm rebuild` is skipped by npm 11's allowScripts gate and by
# the npm-shipped prebuild.
(cd "$(_node_pty_dir)" && npx node-gyp rebuild)
# Mirrors upstream nix/desktop-package.nix — tsc workspaces, Expo web
# export for the renderer, then the desktop main process. No
# electron-builder involved.
npm run build:server
# Daemon Web UI (browser build, precompressed) into
# packages/server/dist/server/web-ui, as upstream's nix/package.nix does.
# Official desktop builds serve resources/app-dist instead; under system
# Electron that path points into electron*/resources, so the daemon falls
# back to web-ui and returned 404 without this. Runs before the Electron
# renderer export below, since both write packages/app/dist.
npm run build:daemon-web-ui
npm run build --workspace=@getpaseo/expo-two-way-audio
(cd packages/app && PASEO_WEB_PLATFORM=electron npx expo export --platform web)
npm run build:main --workspace=@getpaseo/desktop
}
package() {
cd "${pkgname}-${pkgver}"
# Runtime closure by static module-graph tracing: upstream's daemon/CLI
# trace (PASEO_TRACE_DESKTOP=1 also pulls in the Electron main process and
# its preloads) plus our supplement, instead of the 2 GB node_modules.
PASEO_TRACE_DESKTOP=1 node scripts/trace-daemon.mjs > "${srcdir}/runtime-files.txt" \
2> "${srcdir}/trace-stderr.log" \
|| { cat "${srcdir}/trace-stderr.log" >&2; return 1; }
node "${srcdir}/trace-desktop.mjs" >> "${srcdir}/runtime-files.txt" \
2>> "${srcdir}/trace-stderr.log" \
|| { cat "${srcdir}/trace-stderr.log" >&2; return 1; }
# Known-benign trace noise is dropped; anything unexpected still prints.
# ws/node-fetch probe optional native accelerators (bufferutil,
# utf-8-validate, encoding) that the lockfile never installs — pure-JS
# fallbacks are used. nft also tries to parse non-JS assets it carries
# (a stray src/*.ts, zsh shell-integration dotfiles) and logs the
# resulting token errors.
grep -vE "utf-8-validate|bufferutil|[\"']encoding[\"']|Failed to parse .*(packages/server/src/|shell-integration)|^Unexpected token" \
"${srcdir}/trace-stderr.log" >&2 || true
local _libdir="${pkgdir}/usr/lib/paseo"
install -d "${_libdir}"
# Root manifest lets node resolve the workspace layout at runtime.
cp package.json "${_libdir}/"
# Copy file-by-file, preserving workspace symlinks
# (node_modules/@getpaseo/* -> packages/*). Directory entries are skipped:
# their contents are listed individually, and copying a directory onto a
# path that already exists would nest instead of merge.
# node-pty's npm-shipped prebuilds are dropped: its loader prefers the
# build/Release addon we compile ourselves (lib/utils.js search order).
sort -u "${srcdir}/runtime-files.txt" | grep -v 'node_modules/node-pty/prebuilds/' | \
while IFS= read -r f; do
if [ ! -e "$f" ] && [ ! -L "$f" ]; then
continue
fi
if [ -d "$f" ] && [ ! -L "$f" ]; then
continue
fi
install -d "${_libdir}/$(dirname "$f")"
cp -a "$f" "${_libdir}/$f"
done
# Under `electron <dir>` (default_app), Electron derives the Wayland app_id
# from the loaded package.json "name" *before* main.js runs, so the in-app
# app.setName("Paseo") is too late and the window reports "getpaseo-desktop"
# (sanitised @getpaseo/desktop) — GNOME then can't match it to paseo.desktop
# and shows no icon. Rename the installed manifest so the app_id is "Paseo",
# matching StartupWMClass= (and paseo-bin). Runtime module resolution is
# path-based via the node_modules/@getpaseo/* symlinks, so the name field is
# never read at runtime; npm tooling that does isn't present post-install.
local _desktop_manifest="${_libdir}/packages/desktop/package.json"
sed -i 's#"name": "@getpaseo/desktop"#"name": "Paseo"#' "${_desktop_manifest}"
if grep -q '"name": "@getpaseo/desktop"' "${_desktop_manifest}"; then
printf 'ERROR: failed to rewrite desktop package.json name for the Wayland app_id\n' >&2
return 1
fi
# Launcher tracks _electron_pkg so an Electron bump is a one-line change.
install -d "${pkgdir}/usr/bin"
sed "s/@ELECTRON@/${_electron_pkg}/g" "${srcdir}/paseo.sh" > "${pkgdir}/usr/bin/paseo"
chmod 755 "${pkgdir}/usr/bin/paseo"
install -Dm644 "${srcdir}/paseo.desktop" \
"${pkgdir}/usr/share/applications/paseo.desktop"
# Session-scoped daemon launcher (node CLI entry, full login-shell env) and
# the static user unit that runs it. Started via XDG autostart, not enabled
# — see paseo.install.
install -Dm755 "${srcdir}/paseo-daemon-session.sh" \
"${pkgdir}/usr/bin/paseo-daemon-session"
install -Dm644 "${srcdir}/paseo.service" \
"${pkgdir}/usr/lib/systemd/user/paseo.service"
install -Dm644 packages/app/dist/pwa-icon-192.png \
"${pkgdir}/usr/share/icons/hicolor/192x192/apps/paseo.png"
install -Dm644 packages/app/dist/pwa-icon-512.png \
"${pkgdir}/usr/share/icons/hicolor/512x512/apps/paseo.png"
install -Dm644 LICENSE "${pkgdir}/usr/share/licenses/${pkgname}/LICENSE"
# Guard against trace regressions: every path here is load-bearing for
# the CLI, the daemon spawn chain, or the renderer. A miss fails the
# build instead of surfacing as a runtime error after install.
local _required=(
usr/bin/paseo
usr/bin/paseo-daemon-session
usr/lib/paseo/package.json
usr/lib/paseo/packages/cli/bin/paseo
usr/lib/paseo/packages/desktop/dist/main.js
usr/lib/paseo/packages/desktop/dist/preload.js
usr/lib/paseo/packages/desktop/dist/features/browser-keyboard/guest-preload.js
usr/lib/paseo/packages/desktop/dist/daemon/node-entrypoint-runner.js
usr/lib/paseo/packages/desktop/package.json
usr/lib/paseo/packages/cli/dist/index.js
usr/lib/paseo/packages/cli/dist/run.js
usr/lib/paseo/packages/server/dist/scripts/supervisor-entrypoint.js
usr/lib/paseo/packages/server/dist/server/server/exports.js
usr/lib/paseo/node_modules/@getpaseo/cli
usr/lib/paseo/node_modules/@getpaseo/server
"usr/lib/paseo/$(_node_pty_dir)/build/Release/pty.node"
usr/lib/paseo/packages/app/dist/index.html
usr/lib/paseo/skills
# 0.11.0 built-in plugins, compiled by the daemon at runtime (esbuild)
# and resolved relative to the server dist, not through the module
# graph.
usr/lib/paseo/packages/server/dist/server/builtin-plugins/claude-usage-source/paseo-plugin.json
# Daemon Web UI, served from disk rather than through the module graph.
usr/lib/paseo/packages/server/dist/server/web-ui/index.html
# Hardcoded by system-electron-paths.patch: the window icon and the
# editor-target icons. Upstream reads both out of process.resourcesPath,
# which under system Electron points into electron*/resources instead of
# here, so the patch redirects them and these must ship.
usr/lib/paseo/packages/desktop/assets/icon.png
usr/lib/paseo/packages/desktop/assets/editor-targets/vscode.png
)
local _f
for _f in "${_required[@]}"; do
if [[ ! -e "${pkgdir}/${_f}" ]]; then
printf 'ERROR: runtime closure check failed: missing %s\n' "${_f}" >&2
return 1
fi
done
}
Changes since previous scan
--- PKGBUILD @ 2026-10-30 00:29+++ PKGBUILD @ 2026-11-01 00:21@@ -1,7 +1,7 @@ # Maintainer: jinzhongjia <mail@nvimer.org> pkgname=paseo-pkgver=0.13.0+pkgver=0.13.2 pkgrel=1 pkgdesc="One interface for all your Claude Code, Codex and OpenCode agents (built from source, runs on system Electron)" arch=('x86_64')@@ -26,7 +26,7 @@ 'trace-desktop.mjs' 'system-electron-paths.patch' )-sha256sums=('a7d9b61ecc94316a125f36539a58fe230dfc336722aaed8e86b89505d594019f'+sha256sums=('07023ced1de45a32db63a50b8989e86217c09750d5fe7cacba20ca80dfec1d8a' 'f9e194a879a87d87021ad06f489a9e4197ded629055a38e2da3557a423785de4' '8219842d8f7bb20e0608450f2574fa37d98e37ef410de459e0792651a78c1b47' 'df0d01b98ac405c5c25edbb91d61bb9e05355a57e0e652e00823d6331618d686'Scan history
| Scanned at (UTC) | Severity | Rules |
|---|---|---|
| 2026-11-01 00:21:48 | Low | 2 |
| 2026-10-31 00:07:41 | Low | 2 |
| 2026-10-30 05:55:14 | Medium | 1 |
| 2026-10-30 00:29:30 | Low | 2 |
| 2026-10-29 00:13:55 | Low | 2 |
| 2026-10-28 00:06:54 | Low | 2 |
| 2026-10-27 05:46:44 | Medium | 1 |
| 2026-10-27 00:23:28 | Low | 2 |
| 2026-10-26 05:43:49 | Medium | 1 |
| 2026-10-26 00:12:40 | Low | 2 |
| 2026-10-25 00:20:48 | Low | 2 |
| 2026-10-24 00:14:55 | Low | 2 |
| 2026-10-23 00:02:19 | Low | 2 |
| 2026-10-22 00:13:10 | Low | 2 |
| 2026-10-21 00:12:50 | Low | 2 |
| 2026-10-20 17:28:05 | Medium | 1 |
| 2026-10-20 00:10:31 | Low | 2 |
| 2026-10-19 07:24:05 | Medium | 1 |
| 2026-10-19 00:03:55 | Low | 2 |
| 2026-10-18 00:06:55 | Low | 2 |