All plugins

plugin-safehouse

Runs one local Hermes plugin inside a disposable, canary-seeded macOS profile; invokes its registered handlers and reports observed file, environment, network, process, persistence, and model-context behavior as FLAGGED, REVIEW, INCONCLUSIVE, or NO_FLAGS_OBSERVED. Disclosure — the model-callable `safehouse_trial` tool (and `/safehouse`) executes the chosen local plugin's register() and handlers without a Hermes approval prompt, inside a short-lived macOS sandbox-exec worker with a scratch HOME/HERMES_HOME, fake canary credentials and a minimal environment; the sandbox profile is allow-default with network, fork/exec, signals to other processes, LaunchServices and writes outside a temporary trial root denied, so files outside the user's home and non-LaunchServices system services remain reachable; the trial root is removed afterwards. The report returned to the model includes short candidate-controlled text (handler return values up to 300 chars, worker stderr tail up to 1200 chars) with canary tokens redacted. It imports PluginManager and calls the private _scan_directory/_load_plugin loader inside the disposable worker so the candidate registers through Hermes' real loader; this is a disclosed compatibility risk. The installed plugin makes no third-party network calls, stores no credentials, sends no telemetry, and starts no long-running background process. Optional repository-only launch scripts invoke local ffmpeg.

LaithAskarToolsv0.1.0Updated 32 minutes ago

About

Runs one local Hermes plugin inside a disposable, canary-seeded macOS profile; invokes its registered handlers and reports observed file, environment, network, process, persistence, and model-context behavior as FLAGGED, REVIEW, INCONCLUSIVE, or NO_FLAGS_OBSERVED. Disclosure — the model-callable `safehouse_trial` tool (and `/safehouse`) executes the chosen local plugin's register() and handlers without a Hermes approval prompt, inside a short-lived macOS sandbox-exec worker with a scratch HOME/HERMES_HOME, fake canary credentials and a minimal environment; the sandbox profile is allow-default with network, fork/exec, signals to other processes, LaunchServices and writes outside a temporary trial root denied, so files outside the user's home and non-LaunchServices system services remain reachable; the trial root is removed afterwards. The report returned to the model includes short candidate-controlled text (handler return values up to 300 chars, worker stderr tail up to 1200 chars) with canary tokens redacted. It imports PluginManager and calls the private _scan_directory/_load_plugin loader inside the disposable worker so the candidate registers through Hermes' real loader; this is a disclosed compatibility risk. The installed plugin makes no third-party network calls, stores no credentials, sends no telemetry, and starts no long-running background process. Optional repository-only launch scripts invoke local ffmpeg.

Plugin Safehouse

Plugin Safehouse runs one local Hermes plugin in a macOS sandbox with planted fake secrets and reports what the plugin did, what it tried, and how much of it was exercised.

Install

Install directly from GitHub:

hermes plugins install LaithAskar/hermes-plugin-safehouse
hermes plugins enable plugin-safehouse

Until the catalog submission is merged, Hermes labels this as a custom, unreviewed source. You can also clone the repository and run the CLI without installing anything:

git clone https://github.com/LaithAskar/hermes-plugin-safehouse.git
cd hermes-plugin-safehouse
python3 -m safehouse.cli trial tests/fixtures/benign

From another directory, set PYTHONPATH=/path/to/hermes-plugin-safehouse.

Requires macOS and a local Hermes install. Safehouse finds Hermes at ~/.hermes/hermes-agent. To use another copy, set SAFEHOUSE_HERMES_ROOT; to bypass the launcher probe as well, set all three of SAFEHOUSE_HERMES_ROOT, SAFEHOUSE_HERMES_PYTHON, and SAFEHOUSE_HERMES_SITE_PACKAGES (:-separated).

Dynamic trials support manifest kind: standalone (the default) and kind: exclusive (memory providers: register() runs, the provider lifecycle does not). Other kinds (backend, model-provider, platform, which most of Hermes' bundled plugins use) and portable plugin.json packages, are refused at preflight and reported INCONCLUSIVE.

Security and catalog disclosures

  • Safehouse launches a short-lived Python worker under macOS sandbox-exec. The sandbox denies candidate network access, process spawning, signals to other processes, reads under the real home, and writes outside the disposable trial root.
  • It reads the candidate plugin directory and the local Hermes runtime, then stages the candidate in a temporary fake HOME and HERMES_HOME. It plants fake credentials only; it does not copy real credentials into the trial.
  • The worker imports Hermes' PluginManager and calls its private _scan_directory loader inside the disposable process so the candidate registers through Hermes' real loader. This is an explicitly disclosed compatibility risk and may require updates when Hermes changes that private seam.
  • The plugin stores no credentials, sends no telemetry, makes no third-party network calls, and starts no long-running background process. Optional launch-asset scripts invoke local ffmpeg; they are not used by the installed plugin.

It differs from hermes-plugin-guard, which statically scans plugin source before install: Safehouse runs the plugin and watches its behavior, so the two answer different questions.

What a receipt looks like

sleeper  FLAGGED  red flags: 3  all during invoke of tool:sleeper_lookup
  canary file read  file:home/.ssh/id_ed25519                   __init__.py:16
  canary env read   env:OPENAI_API_KEY                          __init__.py:14
  token in egress   env:OPENAI_API_KEY in urllib.Request args   __init__.py:22
  Coverage: 1 of 1 handlers called
  What it does (observed capabilities; a person decides what they mean):
    network      connect to 127.0.0.1:9 (seen at urllib.Request, http.client.connect, socket.getaddrinfo, socket.connect)  (denied by the sandbox: yes)
    subprocess   /usr/bin/true  (denied by the sandbox: yes)

NO_FLAGS_OBSERVED is not a safety verdict: it says nothing was flagged in this trial. Every report says what was not observed.

What it catches

Red flags (FLAGGED, exit 1):

  • A planted secret was touched without being declared, matched by file identity (inode), not path: a hard link, a rename, a symlink, or a dir_fd open still counts. Planted files are ~/.ssh/id_ed25519, ~/.aws/credentials, ~/.config/gh/hosts.yml, HERMES_HOME/.env, HERMES_HOME/auth.json, and HERMES_HOME/state.db, plus the env keys OPENROUTER_API_KEY, ANTHROPIC_API_KEY, OPENAI_API_KEY, TELEGRAM_BOT_TOKEN, DISCORD_BOT_TOKEN, and GITHUB_TOKEN.
  • A planted token appears in undeclared network or subprocess arguments, in a file the plugin wrote, in a value a handler returned, or in text that would enter the model's context. A requires_env declaration never covers those last three.
  • Any attempt to read, stat, list, or open something under the real home directory.
  • A signal sent to any other process (the sandbox denies it).
  • A sandbox escape attempt: a write outside the trial or a denied LaunchServices call.
  • Replaced Hermes callables (host_monkeypatch), and enumerating live objects to reach the recorder (evidence_tamper_attempt).

Review items (REVIEW, exit 3) are behavior a person has to judge. The rules live in one table, safehouse/rules.py:

  • Outbound network attempts and process spawns, even though the sandbox denies them. A reverse shell and a GitHub API call look the same here on purpose: Safehouse does not judge hosts or commands.
  • Changes to ~/.ssh/authorized_keys, ~/.ssh/config, ~/.gitconfig, shell rc and profile files, ~/Library/LaunchAgents, and the Hermes control surfaces $HERMES_HOME/{SOUL.md,config.yaml,plugins,hooks,skills,memories,cron}, labelled sensitive_write. The parent-side snapshot also catches changes to the $HOME paths that no audited write explains, such as a write through a symlinked directory.
  • Enumerating the whole environment. This can expose every credential injected into the process even when an encoded token is not recognized later.
  • Querying the system account database with pwd.getpwuid or pwd.getpwnam, which can reveal that HOME is disposable.
  • Instruction-like text that would enter model context, plus pre_tool_call/tool_request results with Hermes' explicit action: modify directive.
  • A secret the manifest declares in requires_env: reading it, and sending it over the network or to a process (declared_env_read, declared_token_egress). Any destination counts the same.
  • A tool or hook missing from a provides_tools or provides_hooks list that exists. Those fields are optional in Hermes, so a manifest without them only gets a warning.

Loading native code (ctypes, CFFI, or a runtime extension) makes the trial INCONCLUSIVE, because audit hooks cannot see inside it.

Everything the plugin did is listed under What it does as an observed capability that requires interpretation, with the phase and the source line: network attempts with the exact layer (connect, bind, getaddrinfo), subprocess argv, temp files, writes under $HOME (user_home_write) and HERMES_HOME, reading the whole environment, native code, and any text that would enter the model's context. Whole-environment reads and instruction-like context become REVIEW findings.

Only what Hermes actually forwards is scanned: ordinary tool results; system prompt sections and ctx.inject_message; the string a transform_tool_result, transform_terminal_output, or transform_llm_output hook returns; context from a pre_llm_call result; the whole request an llm_request middleware returns, since those are provider kwargs (including forged tool_calls); what an llm_execution middleware returns or passes to next_call (choices, messages, reasoning); and what a tool_execution middleware returns. Tool definitions are scanned for title, description, property names, enum, const, default, and examples; schema text appears under What it does only when it contains a review marker, and planted tokens produce a red flag instead.

Each scan is limited by characters (64 KiB), a shared node budget, depth, and wall time, and reads candidate data through unbound built-ins so no plugin method runs. A scan that hits a limit, or finds a value Hermes would have to run plugin code to read (a custom __str__, an overridden get, a key defining its own __eq__), gets a separate context_scan_incomplete inconclusive finding, even when it collected no text.

Coverage: N of M handlers called covers tools, hooks, slash commands, CLI commands, middleware, and callable prompt sections. A handler counts only once the running interpreter shows its body started. A spoofed __signature__, a generator nothing advanced, a call past the cap (256 per trial), or a handler that raises or times out makes the trial INCONCLUSIVE; starting a handler is not enough to claim its behavior was exercised. Registrations are rechecked after the calls finish, so a tool a handler registers is reported, called, and counted too. A changed file that exceeds the bounded canary-token scan also makes the trial INCONCLUSIVE rather than clean.

CLI commands are parsed and dispatched the way hermes <name> does it, once per reachable subcommand path, each with its own entry in M. A path whose required values cannot be synthesized, or past 32 paths per command, is named in the report and forces INCONCLUSIVE. A command with no handler on a path, where its setup callable is all the code there is, stays NO_FLAGS_OBSERVED, with a cli_setup_only note saying what ran.

What it cannot catch

  • Linux and Windows are unsupported. Dynamic trials need macOS sandbox-exec; elsewhere you get a static scan and INCONCLUSIVE.
  • Determined evasion. A plugin that detects the trial can stay dormant. Realistic paths and values raise the bar, they do not remove it. Evidence comes from the process the plugin shares, so a determined plugin can forge or suppress it; the sandbox denial still holds.
  • Native code. Audit hooks cannot see inside ctypes, CFFI, or extension modules. A canary read through libc is not detected, so reaching a native bridge makes the trial INCONCLUSIVE rather than clean.
  • Whether a reviewed behavior is acceptable. REVIEW reports what was attempted. It does not know whether a host, a command, or the destination of a declared token is legitimate.
  • Plugin kinds beyond standalone and exclusive. See Install.
  • Behavior gated on real data or time. Handlers run once with synthetic arguments. Delayed threads, triggers, and session state stay dark.
  • Return-value meaning beyond the checks above. Return values are scanned for planted tokens and a short phrase list, not interpreted generally. Text inside non-standard schema keywords that hold objects is not scanned. For pre_tool_call and tool_request, Safehouse recognizes Hermes' explicit action: modify directive but does not judge the rewritten arguments.
  • CLI paths that need real values. A handler behind a required value the parser rejects synthetically is reported as not reached rather than run.
  • Encoded or split tokens and socket payloads (only addresses are visible). Direct reads through os.environ, os.environb, and posix.environ are still recorded before encoding. Reports show content-free return summaries, so an encoded planted token is not copied into report output.
  • Exclusive-provider lifecycles. register() runs, but provider methods are not driven; those trials are INCONCLUSIVE.
  • Shipped bytecode. .pyc files and __pycache__ directories are excluded. The report warns that the trial ran the staged source-only package. Native artifacts and .pyo files remain rejected.

Reproducible benchmark

scripts/verify.py is the bundled mini-corpus. It runs known-benign, known-malicious, review-required, blind-spot, and timeout fixtures under the real dynamic trial and checks the expected verdicts:

| fixture | behavior | expected verdict |
|---|---|---|
| benign | declared echo tool | NO_FLAGS_OBSERVED |
| deceptive | canary access during registration and calls | FLAGGED |
| sleeper | looks clean at registration, steals planted secrets when invoked | FLAGGED |
| hook_abuser | abuses a registered hook | FLAGGED |
| invoke_modes | one handler raises and another times out | INCONCLUSIVE |
| attacks/oversized_write | changed file exceeds the canary scan cap | INCONCLUSIVE |
| attacks/review_reverse_shell | shell and outbound network attempt | REVIEW |
| attacks/libc_canary_read | native-code blind spot | INCONCLUSIVE |
| attacks/cffi_canary_read | CFFI native-code blind spot | INCONCLUSIVE |
| attacks/malformed_url | malformed outbound destination | REVIEW |
| attacks/tool_result_injection | instruction-like ordinary tool result | REVIEW |
| attacks/unreadable_write | changed file cannot be snapshotted | INCONCLUSIVE |
| hanging | handler exceeds its budget | INCONCLUSIVE |

Run PYTHONDONTWRITEBYTECODE=1 python3 scripts/verify.py; it exits nonzero if any verdict, containment probe, cleanup check, or redaction check differs. tests/fixtures/attacks/ contains the broader adversarial corpus, one reproducible plugin per attack. scripts/corpus.py <corpus.json> remains available for local third-party plugin collections, but no external corpus result is claimed here.

Details

Verdicts

| Verdict | Meaning |
|---|---|
| FLAGGED | At least one red flag. |
| INCONCLUSIVE | No red flag, but the trial could not support a conclusion: a handler was skipped, the worker failed or timed out, evidence broke integrity or overran its limit, native code loaded, preflight or the sandbox refused, or only static mode ran. |
| REVIEW | No red flag and the trial completed, but observed behavior needs a human decision. It is neither a red flag nor a clearance. |
| NO_FLAGS_OBSERVED | Nothing was flagged in this trial. If nothing was callable the receipt says Coverage: registration only, nothing to call. |

Precedence: FLAGGED > INCONCLUSIVE > REVIEW > NO_FLAGS_OBSERVED. Exit codes: 0 NO_FLAGS_OBSERVED, 1 FLAGGED, 2 INCONCLUSIVE, 3 REVIEW, 64 usage error.

Usage

python -m safehouse.cli trial path/to/plugin                 # Markdown summary
python -m safehouse.cli trial path/to/plugin --json          # full JSON report
python -m safehouse.cli trial path/to/plugin --static-only   # preflight + source scan, run nothing
python -m safehouse.cli trial path/to/plugin --no-invoke     # load and register only, call nothing
python -m safehouse.cli trial path/to/plugin --timeout 10 --output ./safehouse-out

Inside Hermes: the safehouse_trial tool takes {"path": str, "static_only"?: bool, "no_invoke"?: bool, "timeout"?: number} and returns {"ok": true, "report": {...}} or {"ok": false, "error": {"code", "message"}}. The /safehouse <plugin-dir> [--static-only] [--no-invoke] [--timeout N] command returns concise Markdown. Safehouse deliberately registers no hermes <subcommand>: Hermes' host isolation skips plugin CLI commands.

How a trial works

  1. Preflight. Never executes candidate code. Rejects symlinks, hard links, special files, path escapes, native artifacts, oversized trees, and malformed manifests.
  2. Staging. The plugin is copied, re-hashed, into a temporary root that looks like an ordinary profile: HOME at <root>/Users/<name>, HERMES_HOME at $HOME/.hermes. The name, paths, probe values, and canary tokens look like a normal machine and contain no tool name.
  3. Containment and smoke check. The SBPL profile denies network, process fork and exec (except the worker interpreter), all writes outside the trial root, reads under the real home except the Hermes and interpreter trees, signals to anything but the worker itself, lsopen, and mach lookups of LaunchServices. Before any plugin code runs, a probe under the exact profile must prove each rule against a real victim process, a real loopback listener, and a real LaunchServices call. If any rule fails, the trial refuses to run and reports INCONCLUSIVE.
  4. Worker. Runs python -I -S -B, installs the audit hook, recording views of os.environ, os.environb, and posix.environ, and watchers for stat, dir_fd opens, native extension loads, account-database lookups, and ctx.inject_message. It loads a no-op plugin first so Hermes' lazy setup is not attributed to the candidate. Then PluginManager._scan_directory, _load_plugin, capture, invoke, unload. Events stream as JSONL over an inherited pipe with a nonce trailer.
  5. Invoke. Every registered tool, hook, slash command, CLI command, middleware, and callable prompt section runs once under a 2 second per-call budget and a total budget, with realistic synthetic arguments. Actual return values are scanned, but the report stores only content-free type and size summaries.
  6. Supervision and compare. The parent owns the timeout and starts the worker in a new session. Evidence is accepted only with contiguous sequence numbers and a matching trailer. Events are attributed to the plugin or the host from stack frames; host-origin events appear only as host activity.

Report shape

TrialReport (safehouse/model.py, schema safehouse.report/v0.6): verdict, mode, target, candidate (including requires_env), findings[] (severity in red_flag/inconclusive/review/warning/observed/info, category, code, message, evidence[], phase, invocation, seqs, site), capabilities[] (kind in network/subprocess/temp_files/user_home_write/sensitive_write/hermes_home_write/plugin_data_write/env_enumeration/native_code/context_injection/trial_probe, detail, phase, invocation, site, seqs, denied), coverage (per-kind registered and called counts, skipped[], native_code[]), invocations[] (kind, name, args, outcome, detail, returned, duration_s, first_seq, last_seq, entered), secrets_touched[], host_activity[], timeline[], declared, observed, event_counts, file_changes[], sandbox, evidence, host_sentinel_unchanged, limitations[], not_observed[], duration_s.

Verification

PYTHONDONTWRITEBYTECODE=1 python -m unittest discover      # dynamic tests skip off macOS
PYTHONDONTWRITEBYTECODE=1 python -O -m unittest discover   # same suite with assertions stripped
PYTHONDONTWRITEBYTECODE=1 python scripts/verify.py         # smoke proofs + fixture trials, receipt
PYTHONDONTWRITEBYTECODE=1 python scripts/corpus.py <corpus.json>
PYTHONDONTWRITEBYTECODE=1 python scripts/demo.py           # add --write-transcript to save docs/demo-transcript.txt

Limitations that apply to every report are listed in the report itself: the audit hook and environ wrapper are evidence, not containment; sandbox-exec is deprecated by Apple and reads outside the real home are allowed; the worker relies on the same private Hermes seams as hermes plugins doctor; and origin attribution can be spoofed by a hostile plugin (the sandbox denial still applies).

README from the reviewed commit 8e2e5ce; it updates when the author re-pins.

Developer Mode

Explore built-in Hermes toolsets and actions.

HermesAgent

Developer?

Create a Hermes extension for Desktop or agent workflows.

Developer Section