Skip to content

feat(install): one-click privileged CLI install via authenticated osascript (#4)#5

Merged
dakl merged 7 commits into
mainfrom
claude/issue-4-privileged-helper
Jun 23, 2026
Merged

feat(install): one-click privileged CLI install via authenticated osascript (#4)#5
dakl merged 7 commits into
mainfrom
claude/issue-4-privileged-helper

Conversation

@dakl

@dakl dakl commented Jun 22, 2026

Copy link
Copy Markdown
Owner

What

Long-term fix for #4: the app's Install CLI button can now write
/usr/local/bin/engram on Macs where that dir is root-owned (Apple Silicon,
fresh macOS), instead of failing with permission-denied.

It performs the symlink with one authenticated prompt, by running the
operation through the Apple-signed /usr/bin/osascript:

do shell script "… ln -sfn <bundled engram> /usr/local/bin/engram"
    with administrator privileges
  • One native password dialog, then nothing left behind — no daemon, no
    Login Items entry, no helper tool, no XPC, no extra entitlement.
  • Both symlink endpoints are app-derived (never client input) and still
    shell-quoted + AppleScript-escaped.
  • Cancelling returns the sheet to confirm; failure surfaces the error plus a
    runnable sudo … install terminal fallback.

See ADR 0022 for the full decision, including the options survey.

Note on the history (please squash-merge)

This branch took a detour: it first prototyped an SMAppService + XPC
privileged daemon (7814acb), then removed it (08ae311) once it was clear a
persistent root daemon + Login Items toggle is the wrong shape for a one-shot
symlink. The osascript route also does not offer Touch ID (corrected in
3b2fa83 after on-device testing). Squash-merge to keep that detour out of
main's history.

Relationship to other work

  • The short-term fix (engram install → symlink + clearer error) lives on
    claude/issue-1-discussion-33lhp5. This PR largely supersedes it — consider
    closing that branch or reconciling the Setup.swift/InstallSheet.swift
    overlap before/after merge.
  • The in-flight ADR 0023 (recall cooldown) work is not in this PR.

Verification

  • make test green (97 incl. new coverage); make app builds & signs.
  • The osascript flow is just a subprocess — exercised on a local signed build
    (one password dialog, symlink created). No notarization needed to test.

🤖 Generated with Claude Code

dakl and others added 7 commits June 22, 2026 19:57
The app's "Install CLI" button writes /usr/local/bin/engram, which is
root-owned on Apple Silicon and fresh macOS, so it failed with permission
denied. Add an SMAppService + XPC privileged helper that creates the symlink
as root: the bundled engram CLI doubles as the daemon via a hidden
_helper-daemon subcommand, registered from a LaunchDaemon plist and reached
over a Mach service. The daemon takes no client-supplied paths and validates
the caller's code signature before acting. On first use macOS routes the user
to System Settings -> Login Items; declining falls back to sudo in Terminal.

Long-term fix for #4; see ADR 0022. Runtime behaviour (daemon registration,
approval, XPC code-sign check) only exercises on a signed, notarized build.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The SMAppService LaunchDaemon was the wrong shape for a one-shot symlink: it
registers a persistent root daemon and forces a System Settings → Login Items
toggle. Replace it with a single authenticated command run through the
Apple-signed /usr/bin/osascript (do shell script … with administrator
privileges), which shows one native Touch ID / password dialog and leaves no
daemon, login item, or helper tool behind. Because the requesting process
(osascript) is Apple-signed, the auth dialog offers Touch ID when enabled.

Removes the XPC protocol/daemon, the LaunchDaemon plist, the hidden
_helper-daemon subcommand, and the Login Items approval UI. Rewrites ADR 0022
to record the decision (the daemon was prototyped, then rejected as overkill).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
On-device testing showed `do shell script … with administrator privileges`
always presents a password dialog, not Touch ID — it doesn't route through the
biometric authorization path even though osascript is Apple-signed. Fix the
overstated Touch ID claim in ADR 0022, README, the install sheet, and the
PrivilegedInstaller doc comment; note that Touch ID would require a privileged
helper, which this approach deliberately avoids.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Brings the terminal `engram install` into line with the app's privileged
install: symlink /usr/local/bin/engram (no more copy/version-drift), and on a
non-writable dir throw a clear "run sudo engram install" message instead of a
raw NSError. This is also the command the app's failure fallback points users
at. Ported from the short-term fix branch (claude/issue-1-discussion-33lhp5),
which this folds in and supersedes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…n.swift

The first install commit's `git add -A` pulled a concurrent session's ADR 0023
recall-cooldown changes into main.swift, but the matching MemoryStore methods
never landed on this branch — breaking the build (no member
recentlyInjectedInSession). The install work's net change to main.swift is nil,
so restore it to main's version. The 0023 feature stays separate.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ording

- Reorder EngramModel so the bundledEngramPath/runBundledEngram doc comments
  hug their own declarations (the new property had split them).
- Add a 120s watchdog to the osascript Process so a wedged auth dialog can't
  hang the install sheet's spinner; report a timeout via terminationReason.
- Fix stale "privileged helper" wording on InstallKind.usesPrivilegedHelper —
  it's the authenticated osascript path now, no helper.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Add EngramTests for PrivilegedInstaller.shellQuoted / appleScriptEscaped —
  the security-relevant, pure, testable piece of the privileged install
  (single-quote escaping, spaces, backslash-then-quote ordering). Helpers made
  internal for @testable access.
- Drop the locale-fragile "cancel" substring from cancellation detection; rely
  on osascript's -128 (userCancelledErr) alone.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@dakl dakl merged commit 7ef8bcd into main Jun 23, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant