Agent attach (experimental)¶
Spectre's :agent module lets you attach to a running Compose Desktop JVM and drive
its UI from a separate process — no HTTP listener, no network port, no need to mount routes
at target startup.
Target shape (two paths, one API):
- Preferred — preinstalled
spectre-coreon the target (instrumented attach). Bootstrap findsComposeAutomatoron the app classpath. Use this whenever you control the target build. - Inject — no preinstalled core (experimental inspect). The loadable
spectre-agent-runtimejar carries a nestedMETA-INF/spectre/inject-runtime.jar; bootstrap loads Spectre core from that payload when the target only has Compose/Skiko. SameAgentAttach.attach(pid)call — no separate “inject flag”. Fine for attach → dump → detach (e.g. stock IntelliJ); prefer preinstalled core for sustained or high-frequency use. Details: Injection without preinstalled core.
This is the right transport when:
- Your test JVM and the UI JVM are different processes by design, but you don't want to modify the UI app's startup wiring.
- You want to inspect a long-running Compose Desktop app interactively through the
spectreCLI or an MCP client. - You're attaching to an IntelliJ-hosted Compose surface from a sister process (often via inject when the IDE build does not ship Spectre). See IntelliJ-hosted Compose for VM options and the stock-IDE recipe.
For comparison with the other transports, see Cross-JVM access (HTTP) and
IntelliJ-hosted Compose (in-process via intellij-ide-starter). Which
operations are Supported vs Unsupported by design vs Not yet CI-executed is
tracked in the capability matrix — every Supported cell must have
executable CI evidence.
Experimental API
Everything under dev.sebastiano.spectre.agent.* is annotated
@ExperimentalSpectreAgentApi and requires explicit opt-in. The API may change in any
release until the UX stabilizes. See Stability policy.
Trust boundary
The agent transport is local only and intended for trusted dev/test environments. Trust model:
- Communication is over a Unix Domain Socket under a short private directory (
/tmp/on Linux/macOS,%TEMP%on Windows). Filesystem permissions are the only access control — directory mode 0700 / socket mode 0600 on POSIX, or an owner-only ACL on Windows/NTFS. - The attaching JVM must run as the same OS user as the target JVM.
- There is no authentication and no encryption on the wire.
- The published
spectre-agentAPI jar is for the attaching JVM. Thespectre-agent-runtimejar gets loaded into the target JVM. See Artifact roles below.
See Security notes for the full risk register.
Requirements¶
- JDK 21+ on both the attaching and target JVMs.
- The attaching JVM must be a JDK (not a JRE) with the
jdk.attachmodule on the module graph. - The target JVM needs a Compose Desktop host (so Spectre can find semantics owners).
Prefer a preinstalled
spectre-coredependency on the target for production-style attach. If core is absent, the agent runtime can inject a nestedMETA-INF/spectre/inject-runtime.jarpayload (experimental inspect path — see Injection without preinstalled core). - Linux, macOS, and Windows. The transport uses native Unix Domain Sockets (
AF_UNIX) on all three — no named pipes, no extra dependencies. Windows requires Windows 10 version 1803 / Windows Server 2019 or newer, when nativeAF_UNIXlanded; older Windows fails the attach preflight with a clear message. - The target JVM should be started with
-XX:+EnableDynamicAgentLoading. Without it, attach prints a stderr warning per JEP 451 and a future JDK will reject the attach entirely. Spectre's launch harness adds the flag for processes it starts; stock apps and IDEs need it in their own VM options (see IntelliJ-hosted Compose).
Artifact roles¶
Agent attach involves two JVMs:
- Target JVM — the Compose app you want to inspect or drive.
- Attacher JVM — the test, inspector, or tool process that calls
AgentAttach.attach(pid).
Preferred target shape — preinstall Spectre core so bootstrap uses the instrumented path (no inject classloader):
// build.gradle.kts of the target application
dependencies {
implementation("dev.sebastiano.spectre:spectre-core:<version>")
// No `spectre-agent` or `spectre-agent-runtime` dependency is needed in the target.
// The attacher supplies the runtime jar to the JDK Attach API.
}
If the target cannot take that dependency (stock IDE, third-party binary), attach still works when Compose is present and the runtime jar carries the nested inject payload — see below.
The attacher JVM usually needs two artifacts:
spectre-agent— the normal API jar that your test/inspector code compiles against.spectre-agent-runtime— the loadable Java-agent runtime jar that gets passed toVirtualMachine.loadAgent(...).
The easiest Gradle shape is a normal implementation dependency plus a runtime-only dependency on the loadable runtime artifact:
dependencies {
implementation("dev.sebastiano.spectre:spectre-agent:<version>")
runtimeOnly("dev.sebastiano.spectre:spectre-agent-runtime:<version>")
}
AgentAttach resolves the loadable runtime jar in this order:
AttachOptions.agentJarPath-Ddev.sebastiano.spectre.agent.runtimeJar=<path>- Classpath auto-discovery of a physical
spectre-agent-runtime-<version>.jar - In-repo fallback at
<spectre-checkout>/agent-runtime/build/libs/agent-runtime-*.jar, only when the attacher cwd is inside a Spectre source checkout (detected via monorepo markers). Published consumers should use options 1–3; the fallback is a Spectre-dev convenience.
In normal Gradle usage, runtimeOnly(...) makes Gradle launch the attacher with the runtime jar
listed in java.class.path; Spectre scans that classpath, takes the physical jar path, and passes
that path to VirtualMachine.loadAgent(...). The attacher does not call classes from the runtime
jar directly, and the target still does not need spectre-agent-runtime declared as a dependency.
Classpath and directory discovery require exactly one runtime-jar candidate. If more than one
spectre-agent-runtime-*.jar / agent-runtime-*.jar is present, attach fails with
AmbiguousAgentRuntimeJarException naming every candidate rather than picking by classpath
order. Use AttachOptions.agentJarPath or -Ddev.sebastiano.spectre.agent.runtimeJar to choose
explicitly.
How attach works¶
AgentAttach.attach(pid) performs this sequence:
- Resolve the loadable
spectre-agent-runtime-<version>.jar. - Create a fresh Unix Domain Socket path such as
/tmp/sp-a-<pid>-<8char-uuid>/agent.sock. - Run attach preflights, including the same-OS-user check.
- Call
VirtualMachine.attach(pid).loadAgent(runtimeJarPath, udsPath). - The target JVM loads the runtime jar and invokes
SpectreAgent.agentmain(...). - Inside the target JVM, bootstrap locates or injects
ComposeAutomator(see below), creates an in-process automator, and starts an IPC server on the UDS path. - The attacher connects an
IpcClientto that socket and returnsAttachedAutomator.
After that, calls such as windows(), findByTestTag(...), click(...), and screenshot() are
small CBOR requests over the socket. They execute inside the target JVM against the in-process
automator, then return DTOs or bytes to the attacher.
Injection without preinstalled core¶
Bootstrap order inside the target:
- Prefer a preinstalled
ComposeAutomatoralready on the target classpath (instrumented attach). - Else extract the nested
META-INF/spectre/inject-runtime.jarfrom the agent runtime jar, open a child classloader parented at a Compose host loader, and load Spectre core from that payload (inject attach). Compose / Skiko stay on the target; only Spectre core and relocated kotlinx bits come from the inject jar.
Inject is an experimental inspect path: fine for rare attach → dump → detach sessions, not for high-frequency CI attach loops. Prefer preinstalled core when you control the target build. Class unload after detach is GC-dependent; do not treat inject as a leak-free production mode.
The same AgentAttach.attach(pid) API is used for both paths — there is no separate
“inject flag” on the public attach surface.
Custom runtime jar path¶
Classpath auto-discovery is the default for normal Gradle runs, but AttachOptions.agentJarPath
and -Ddev.sebastiano.spectre.agent.runtimeJar=<path> are explicit overrides and win before the
classpath scan. Use them for custom launchers, shaded tools, module-path launches, and ad-hoc
scripts that hide the physical runtime jar from java.class.path.
import dev.sebastiano.spectre.agent.AgentAttach
import dev.sebastiano.spectre.agent.AttachOptions
import java.nio.file.Path
AgentAttach.attach(
pid = targetPid,
options =
AttachOptions(
agentJarPath = Path.of("/abs/path/to/spectre-agent-runtime-<version>.jar"),
),
)
Equivalent: set -Ddev.sebastiano.spectre.agent.runtimeJar=<path> on the attacher's JVM.
When the attacher process is running from inside a Spectre source checkout (or a subdirectory of
one), AgentAttach also falls back to
<checkout>/agent-runtime/build/libs/agent-runtime-*.jar so local manual recipes keep working after
./gradlew :agent-runtime:jar. That path is not enabled for arbitrary application working
directories — published consumers should put spectre-agent-runtime on the attacher classpath or
pass an explicit path/system property.
Consumers that cannot use the published Maven coordinate still have two supported paths:
-
As a project dependency (you're inside the Spectre repo or a Gradle composite build that includes it):
-
As an explicit path via
AttachOptions.agentJarPathordev.sebastiano.spectre.agent.runtimeJar, as shown above.
Start the target with the dynamic-agent flag (suppresses the JEP 451 stderr warning):
Attaching¶
In the attaching JVM (typically a test process), opt in to the experimental API and use
AgentAttach.attach:
@file:OptIn(ExperimentalSpectreAgentApi::class)
import dev.sebastiano.spectre.agent.AgentAttach
import dev.sebastiano.spectre.agent.AttachOptions
import dev.sebastiano.spectre.agent.ExperimentalSpectreAgentApi
import dev.sebastiano.spectre.agent.SpectreProcesses
// Find the target by name.
val target = SpectreProcesses.findByName("MyApp").single()
AgentAttach.attach(target.pid).use { automator ->
val windows = automator.windows()
val submitNodes = automator.findByTestTag("Submit")
if (submitNodes.isNotEmpty()) {
automator.click(submitNodes.first().key)
}
// Window/surface attach screenshots fail closed (#359); fullscreen is the only
// screen-pixel capture mode on this path.
val pngBytes = automator.screenshot(fullscreen = true)
} // detach + cleanup on close()
AttachedAutomator is AutoCloseable. Closing it sends an AgentRequest.Detach over the
wire; the agent stops accepting new requests, releases its ComposeAutomator, unlinks
the UDS path, and removes its shutdown hook. A target-side shutdown hook covers crash
cleanup.
AttachOptions¶
AttachOptions(
agentJarPath = null, // null = auto-locate (see "Artifact roles" above)
udsPath = null, // null = <tmp>/sp-a-<pid>-<8char-uuid>/agent.sock (/tmp on POSIX, %TEMP% on Windows)
attachTimeoutMs = 5_000, // how long to wait for the agent's IPC server to come up
)
If you override udsPath with a path under an existing directory, you own that parent
directory's permissions. Spectre creates the default per-attach directory and socket owner-only —
mode 0700/0600 on POSIX, an owner-only ACL (owner full control, inherited ACEs dropped) on
Windows — but it does not tighten directories it did not create.
AgentAttach.attach runs a same-user preflight and throws
AttachPermissionDeniedException if the target JVM is owned by a different OS user (the
JDK Attach API only works across attach-compatible same-user processes on POSIX). On
Linux/macOS the preflight prefers numeric UID equality when both sides can be resolved, and
falls back to ProcessHandle usernames when UID lookup is unavailable (#166).
The JEP 451 -XX:+EnableDynamicAgentLoading flag is not verified by Spectre yet — the
JVM itself prints a stderr warning if it's missing, which is the source of truth. A follow-up
can add a reliable preflight via HotSpotDiagnosticMXBean.
Operation set¶
AttachedAutomator exposes the same operations as the HTTP transport, plus detach:
| Method | Wire op | Returns |
|---|---|---|
windows() |
AgentRequest.Windows |
List<WindowSummaryDto> (includes isShowing; delayed-show hosts may appear before they are visible so keys agree with allNodes() — #362) |
allNodes() |
AgentRequest.AllNodes |
List<NodeSnapshotDto> |
findByTestTag(tag) |
AgentRequest.FindByTestTag |
List<NodeSnapshotDto> |
click(nodeKey) |
AgentRequest.Click |
Unit |
doubleClick(nodeKey) |
AgentRequest.DoubleClick |
Unit |
longClick(nodeKey, holdForMs?) |
AgentRequest.LongClick |
Unit |
swipe(...) |
AgentRequest.Swipe |
Unit (node-to-node or screen coords) |
scrollWheel(nodeKey, wheelClicks) |
AgentRequest.ScrollWheel |
Unit |
pressKey(keyCode, modifiers?) |
AgentRequest.PressKey |
Unit |
focusWindow(nodeKey) |
AgentRequest.FocusWindow |
Unit (raise/activate window hosting node) |
typeText(text) |
AgentRequest.TypeText |
Unit |
screenshot(windowIndex?, surfaceId?, fullscreen?) |
AgentRequest.Screenshot |
ByteArray (PNG); only fullscreen=true succeeds on attach — window/surface fail closed (#359) |
capture(windowIndex) |
AgentRequest.Capture |
AtomicCaptureResult |
windowIdentities(windowIndex?) |
AgentRequest.WindowIdentity |
List<WindowIdentityDto> |
waitForNode(...) |
AgentRequest.WaitForNode |
NodeSnapshotDto |
waitForVisualIdle(...) |
AgentRequest.WaitForVisualIdle |
Unit |
waitForIdle(...) |
AgentRequest.WaitForIdle |
Unit (fingerprint wait; no idling-resource registration over attach — #362) |
printTree() |
AgentRequest.PrintTree |
String (human-readable dump — #362) |
screenshot(node) |
AgentRequest.Screenshot(nodeKey) |
ByteArray (PNG of node bounds — #362; native when recording bridge present, else region of boundsOnScreen) |
click(node) |
AgentRequest.Click |
Unit (DTO overload uses [NodeSnapshotDto.key] — #362) |
close() (auto) |
AgentRequest.Detach |
tear-down |
windowIdentities returns native handle/id (when resolvable), window and Compose-surface
bounds in AWT user-space screen coordinates (same space as windows() /
locationOnScreen / Robot), surface bounds relative to the window (crop rect),
per-window affine transform (scaleX/scaleY/translateX/translateY), and a
cropRequired flag when the surface is a subset of the top-level window (title bar or
embedded panel). For device pixels: point (x, y) → (x * scaleX + translateX, y * scaleY +
translateY); scale widths/heights by scaleX/scaleY only (no translation). Daemon-owned
recording (#183) uses this so capture stays on the daemon host rather than over the
transport.
waitForNode, waitForVisualIdle (#201), and waitForIdle (#362) are available over the
agent transport. waitForIdle runs a fingerprint-only wait on the agent's in-target
automator (timeout / quiet / poll; absolute deadline on the wire like waitForNode). It does
not observe idling resources registered on a different automator instance in the app.
Idling-resource registration and withTracing remain in-process-only.
Richer input verbs (doubleClick / longClick / swipe / scrollWheel / pressKey)
are available over agent, HTTP, and daemon/CLI/MCP (#203). focusWindow(nodeKey) raises
and focuses the AWT window hosting a node over attach (#364) — use it before pressKey /
typeText when the attach client is the foreground process and the target app is not.
Wire format¶
Length-prefixed CBOR over the UDS:
DTOs live in dev.sebastiano.spectre.agent.transport.*. Both sides share the same
classes; CBOR's @SerialName discriminators pin each variant in the sealed-interface
hierarchy.
Protocol version handshake (#199)¶
After the UDS connects, the first exchange is always:
- Client →
hellowithprotocolVersion(currently2—ProtocolVersion.CURRENT) - Runtime →
helloAckwith the same version, orerrorwith categoryprotocolMismatch
While the agent API is experimental, compatibility is exact-match. A version
mismatch fails attach with a clear IOException / SpectreAgentException rather than
proceeding and hanging on later frames. From 1.0 the rule may become additive-compatible
(min/max range); that change will bump ProtocolVersion.CURRENT and this section.
Unknown operations¶
A newer attacher that sends an unknown request discriminator (sealed @SerialName the
runtime does not know) receives error with category unsupportedOperation, not a
decode hang or silent close. That is how mixed-version pairs degrade.
Error taxonomy¶
AgentResponse.Error carries a stable category string alongside message:
| Category | Meaning |
|---|---|
unsupportedOperation |
Runtime too old / op not implemented |
protocolMismatch |
Handshake or schema/framing mismatch |
invalidSelector |
Malformed node key or selector |
nodeNotFound |
Well-formed key, no matching node |
timeout |
Deadline exceeded |
cancelled |
Explicit cancel of an in-flight op (#200) |
payloadTooLarge |
Response/request exceeds the frame hard limit (#204) |
inputRejected |
Focus / Robot / permission rejection |
internalError |
Unexpected agent-side failure (default) |
Selectors (#202)¶
Beyond findByTestTag, the agent transport supports:
| Wire op | Notes |
|---|---|
findByText |
text, exact (default true) |
findByContentDescription |
description |
findByRole |
role string (e.g. Button); matches role.toString() |
NodeSnapshotDto includes contentDescriptions, isDisabled, isSelected (HTTP field-set
parity). On-screen bounds stay integer AWT units on the agent; HTTP keeps double
window+screen rects.
Payload limits (#204)¶
Each IPC frame is length-prefixed and hard-capped at 16 MiB (MAX_FRAME_BYTES). This is a
fail-closed policy:
- Responses that encode larger than the cap (e.g. a huge screenshot) are not truncated or
spilled to disk by the agent transport. The runtime replies with
errorcategorypayloadTooLargeand a message that includes the sizes. - The connection stays open; subsequent ops on the same session continue normally.
- Parity CI can rely on deterministic taxonomy behaviour instead of size-threshold flakes.
Spill-to-file for large captures remains a higher-level concern (capture directories / daemon shared FS from #181); the wire layer does not invent a second path for oversized frames.
HTTP maps payloadTooLarge → 413 Payload Too Large.
Long operations and cancel (#200)¶
After Hello, every request is an operation envelope (opId, optional absolute
deadlineEpochMs, body). Responses are correlated by opId so multiple ops can share one
connection.
- Long work (future waits, heavy capture) runs on a worker thread, not the accept loop — so cancel/detach stay responsive.
- Cancel is an explicit wire op (
cancelwith the targetopId), not socket-close. - Closing a CLI/MCP front-end during a long-poll cancels only that front-end connection's
in-flight work. The daemon session stays attached until an explicit
detach(or daemon kill / crash). Reconnect or use the CLI against the same session id — disconnect is not detach and does not leave an undocumented zombie without a recovery path (see lifecycle below). The agent transport itself only detaches on an explicitdetachor handshake failure teardown.
Clients should branch on category (see AgentErrorCategory / SpectreAgentException),
not on free-text message. The HTTP transport maps the same names onto status codes
(invalidSelector → 400, nodeNotFound → 404, unsupportedOperation → 501,
cancelled → 499 Client Closed Request, payloadTooLarge → 413, etc.) via
SpectreErrorCategory in :server.
Schema evolution rules¶
Additive-safe on DTOs:
- New optional fields with defaults on request/response data classes
- New sealed variants with new
@SerialNamevalues (old peers answerunsupportedOperationfor unknown request names)
Not additive-safe without a version bump:
- Renaming or removing fields
- Changing field types or making an optional field required
- Reusing an old
@SerialNamefor a different shape (seescreenshot_v2vs pre-#289screenshot)
Current limitations¶
- Windows needs 10 version 1803 / Server 2019 or newer. That's when native
AF_UNIXlanded; older Windows fails the attach preflight withAttachPlatformUnsupportedException. Hosted GitHubwindows-latestis not a reliable interactive desktop for the Robot-backed attach fixture; the full attach → exercise → detach UI e2e is opt-in on physical Windows desktops (non-UI transport/ACL tests still run on every Windows CI job):
# bash / zsh / cmd
./gradlew :agent:test -Pspectre.agent.attachE2e.allowWindows=true --tests '*AgentAttachIntegration*'
# PowerShell: quote -P… so the shell does not split on the property name
./gradlew :agent:test "-Pspectre.agent.attachE2e.allowWindows=true" --tests '*AgentAttachIntegration*'
waitForNode / waitForVisualIdle are supported over agent IPC (#201) with
shared deadline budgets and cancel. Idling-resource waitForIdle stays in-process only.
- IntelliJ-hosted Compose: the classloader-disambiguation rule (D-14 in the plan) was
designed to handle PluginClassLoader chains but isn't automatically tested yet. If
you hit issues attaching to an IntelliJ-hosted target, file a Spectre issue with the
agent-attach label.
- Runtime jar is separate from the API jar. The normal spectre-agent dependency is not the
jar loaded into the target JVM. Add spectre-agent-runtime, pass
AttachOptions.agentJarPath, or set -Ddev.sebastiano.spectre.agent.runtimeJar=....
Manual verification recipe¶
# Terminal A — start a Compose app that depends on spectre-core (preferred path)
./gradlew :sample-desktop:run
# Find its PID (cross-platform: jps ships with the JDK)
jps -l | grep "dev.sebastiano.spectre.sample.MainKt"
# POSIX alternative: ps -A | grep "…MainKt" | awk '{print $1}'
# Terminal B — attach the agent. The agent's stderr lands in Terminal A.
./gradlew :agent:attachSpike -Ppid=<pid>
The attachSpike task is intentionally separate from :check — it exists for human
verification and is not config-cache compatible.
CLI and MCP¶
The spectre executable is a client for a per-user local daemon. It starts that daemon on
demand, and spectre mcp shares the same daemon and its attached sessions with ordinary CLI
commands. Keep the executable running only through the MCP client: MCP uses its standard input
and output for protocol frames, so do not add a shell wrapper that prints banners to standard
output.
Start with the same target prerequisites described above, then find and attach it from a shell:
The attach response contains an id. Pass it to commands such as tree, find, click, and
screenshot. On the attach path, screenshot requires --fullscreen for screen-pixel
capture; default / --window / --surface fail closed (occlusion/privacy risk). Without
--output, a successful capture creates a temporary file and prints its path. Prefer a target
with preinstalled spectre-core; experimental inject also works for Compose-only hosts — see
Requirements. Full command and MCP reference: CLI.
Claude Code recipe¶
Install the spectre executable where Claude Code can invoke it, then add it to the project's
.mcp.json. Use an absolute path so Claude Code does not depend on your interactive shell's
PATH:
Restart Claude Code after changing the configuration. It can then use these tools in order:
list_processesto find the target PID.attachwith that PID and retain the returnedsessionId.tree,find, orfind_text/wait_for_nodefor keys, then input tools (click,double_click,long_click,swipe,scroll_wheel,press_key,type_text).screenshotwithfullscreen=truefor an inline full-desktop PNG (window/surface targets fail closed on attach), orcapture/record_*for daemon-filesystem artifacts (paths only).- When the target runs under Compose Hot Reload: call
wait_for_reload_settledbefore triggering a code reload (it must observe the settle chain), then re-runtree/findbefore further input. detachwith the retainedsessionIdwhen finished — releases that session only and returns leftover capture cleanup summary (count/bytes/paths + prune command when any exist). Unknown or already-detached sessions fail closed (isError). Sibling sessions stay attached;detachis never “kill the daemon” or “release all.” Detach does not delete capture files; run the summary’spruneCommandwhen you want cleanup. Preferdetachoverspectre daemon kill(which drops every session). The shared daemon stays up after a successful detach so you canlist_processes/attachagain.
Agent session lifecycle (multi-session + disconnect)¶
- Preferred cleanup order: stop any active recording (
record_stop) → finish or abandon longwait_for_*calls →detachthatsessionId. Concurrent ops while detach runs fail closed (actionable errors; no hang); do not rely on them succeeding mid-teardown. - Two sessions: attach A and B independently; detaching A leaves B usable. Post-detach
tree/click/screenshoton A fail closed with session-not-found honesty. - Client disconnect ≠ detach. Killing or restarting the MCP front-end (stdio death, agent
process exit) cancels in-flight front-end work only. The daemon keeps the target session
until an explicit
detach, CLIspectre detach <session-id>, orspectre daemon kill. After reconnect, list sessions / reuse the samesessionId, or detach deliberately — disconnect does not create undocumented zombies and does not silently detach. - Recovery after front-end death:
spectreCLI against the same user daemon (ps/ attach if needed /tree/detach), or a new MCP client talking to the same daemon.
Full MCP tool names, input/output schemas, filesystem implications, and capture-mode
distinctions: CLI — MCP. MCP detach success JSON is the daemon Detached shape
(sessionId, not CLI --json id) — see
Detach success body (MCP vs CLI).
Node keys are short-lived: get a fresh key with tree or find after an interaction changes the
UI. On reload-aware sessions, keys are also invalidated after a successful hot reload settle —
see Compose Hot Reload awareness.
If the agent also has Compose Hot Reload’s MCP configured, do not alternate randomly:
If you have HR available and want quick sanity checks while iterating on a live app, use the HR MCP; in any other case, Spectre is the right choice.
Call the MCP detach tool (or spectre detach <session-id> from a shell) to release one
session, or spectre daemon kill to stop the shared daemon and discard all sessions when you are
finished.