Skip to content

fix(isolation, safety): keep CanvasTTY's private data out of agents' reach - #148

Closed
BIackFIame wants to merge 22 commits into
howdeploy:mainfrom
BIackFIame:pr/security-hardening
Closed

BIackFIame wants to merge 22 commits into
howdeploy:mainfrom
BIackFIame:pr/security-hardening

Conversation

@BIackFIame

@BIackFIame BIackFIame commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

Branch pr/security-hardening (head b4b69198, 13 commits; 1ed8ad0f + 2 follow-up commits, fast-forward). It is based on pr/orchestration-reliability because both change the same launch path in TerminalManager. No UI.

Why

When you closed #135 you asked us to drop its UI and keep only the isolation and enforcement hardening for behaviour main already has, as PRs without UI. This PR has that hardening, plus four small fixes from the same audit of existing code (redaction, file-id checks, Hermes reads). Every fix closes a way an agent could read or change data that CanvasTTY keeps private: other accounts' sign-ins, the control token, provider-secrets.bin, gateway sockets, Git config.

What changes for users

Agent isolation (sandbox-exec on macOS, bubblewrap on Linux):

  • Plugin data: plugin data (for example an accounts plugin's CLI homes and tokens) is hidden. Only the CLI home the agent was launched with is reopened.
  • Gateway sockets: on Linux these were writable and taken from any CANVASTTY_*_ADDRESS variable. Now only CanvasTTY's own sockets are bound, read-only.
  • Projects inside plugin data: a project there is opened only where CanvasTTY placed it, so a subagent can no longer be started in another account's home.
  • Projects at HOME: with the project at HOME, CanvasTTY's data folder stays read-only (settings.json, plugins).
  • Git files: the files that redirect Git config and worktrees (commondir, config.worktree, .git/worktrees) are write-protected, and .git, .git/hooks and .git/info cannot be renamed away.
  • Claude Code's temp folder (follow-up): Claude Code keeps Bash-tool output and external-editor files in ${CLAUDE_CODE_TMPDIR:-/tmp}/claude-<uid> and ignores TMPDIR. The layer never made that folder writable, so on main every shell call of an isolated Claude fails with "Operation not permitted". An isolated Claude now gets CLAUDE_CODE_TMPDIR = the launch's own private temp folder (seatbelt and bubblewrap). The shared /tmp/claude-<uid> is deliberately not granted: all the person's Claude sessions use it, and an agent could rewrite another session's task output.

Base protection (without isolation):

  • Broad projects: private data stays private even when the project is the home folder. The per-card Claude Code draft folders (claude-draft, which macOS: editing keys for Claude Code cards (newline, select-all, copy/cut/paste) and UTF-8 locale #145 adds under userData) are listed already; nothing creates them yet.
  • Read-only file tools: Read, Grep, Glob, LS and their Qwen Code and OpenCode equivalents are checked as well.
  • Globs: ** and {a,b} globs are matched correctly.
  • Uncheckable calls: a call the checker cannot analyse is refused instead of allowed.
  • Plugin data (follow-up): plugin-data is now on the private list. An accounts plugin keeps every account's sign-in there, so an agent outside the layer (Manual, or isolation off) could read other accounts' tokens. The card's own selected account home (the one the launch pipeline already validates for the layer) is recorded with the card and counts as its own config folder, like CLAUDE_CONFIG_DIR, while the CLI is still started with it. A project CanvasTTY placed inside plugin data keeps its own files. plugin-data is not a name marker (such a project names it in its own paths).

Other fixes:

  • Redaction: overlapping secrets are masked whole, and credential labels survive masking.
  • File ids: they are compared as bigints, so 64-bit ids are exact. The usage-history reader refuses a file that was swapped during the read.
  • Hermes: rollback-journal databases are read with locks, not as immutable.

Commits

  1. 9a4018f3 fix(isolation): hide plugin data from isolated agents
  2. 253a50e2 fix(isolation): bind only CanvasTTY's own gateway sockets, read-only
  3. ef4fc828 fix(isolation): open plugin data projects only where CanvasTTY placed them
  4. dc96b368 fix(isolation): keep CanvasTTY's data folder read-only inside a project that contains it
  5. 8eb366e3 fix(isolation): write-protect the git files that redirect config and worktrees
  6. a051042d fix(safety): keep CanvasTTY-private data private under broad projects
  7. 0a9ed461 fix(safety): refuse read-only file tools on CanvasTTY-private data
  8. b64348d2 fix(safety): match ** and brace globs, refuse what cannot be checked
  9. 902225aa fix(redaction): mask overlapping secrets whole and keep labels for credential rules
  10. 9d6fffbd fix(files): compare file ids as bigints and preserve the bytes that were read
  11. 1ed8ad0f fix(usage): read rollback-journal Hermes databases with locks, not as immutable
  12. 3f62b7e1 fix(isolation): give isolated Claude Code its own temp folder
  13. b4b69198 fix(safety): keep plugin data private in base protection, except the card's own account home

Commits 1–3 belong together: commit 1 alone is not safe on Linux without commit 2. Commits 6–11 are independent of 1–5 and of each other. 12 and 13 were added after the PR was opened (new commits on top, nothing rewritten): 12 is independent; 13 builds on 1 (account home) and 6 (private list).

Verification

  • macOS: this exact tree (b4b69198), with a fake HOME and a short TMPDIR. Results: helpers, typecheck, full suite 2297 / 2293 / 0 fail / 4 skip, build and audit:secrets pass; startup bundle 1,689,084 bytes, equal to main. The seatbelt tests ran under real sandbox-exec. Earlier head 1ed8ad0f: 2293 / 2289 / 0 / 4.
  • Follow-up tests: an isolated Claude keeps its Bash and editor files in the launch's own temp folder, on both layers; other CLIs are untouched and seatbelt, for real: Claude's scratch folder is in its own temp folder; the shared /tmp/claude-<uid> stays read-only (agent-isolation); plugin data is private: another account's home is refused; the card's own selected home and a project placed there stay its own (base-protection-app-private); base protection: a card's own selected account home is its own; other accounts' homes and the rest of plugin data stay private (account-home-isolation, through TerminalManager → decisionContext → DecisionHooks for a Claude and a Codex card). All fail without their fix. The existing test real CLIs start inside the layer ran the installed claude --version inside the layer with the new variable (no sign-in, no model call).
  • Linux, follow-up (same VM, normal user): real AgentIsolation → bwrap on b4b69198: a Claude launch gets CLAUDE_CODE_TMPDIR = its bound private temp; mkdir -p $CLAUDE_CODE_TMPDIR/claude-$(id -u)/p/s/tasks + write works; append/create/mkdir in a shared /tmp/claude-<uid> fail with Read-only file system and the host folder is untouched; a Codex launch keeps the person's value (and the same scratch write fails, which is the main behaviour for Claude). The three changed test files pass there (47 pass, 10 macOS-only skips).
  • Linux (Ubuntu 24.04 VM, bubblewrap 0.9, normal user, no sudo): commits 1–5 were checked through the real AgentIsolation and bwrap on their single-fix branches. Hidden plugin data, read-only sockets, a refused unplaced project, a read-only data folder and protected Git files all behaved as described. Base protection had live bwrap checks on an earlier head. The full VM suite on the integration fails only the three tests that also fail on main there.
  • Screenshots: none needed (no UI).
  • Not verified: Windows. A live Claude Code session (Bash tool, Ctrl+G) inside the layer. Hermes was not run live. The bigint precision case cannot be produced on APFS, so it is covered by reasoning.

Risks

  • Plugin files: a plugin that passes another file of its data in env or args loses access to it under isolation ({launchFiles} is the supported way).
  • Accounts plugins: an accounts plugin must point the CLI home at a real, non-symlinked folder inside its own data.
  • Projects at HOME: with the project at HOME, an agent can no longer write into a project stored inside userData. On Linux, git worktree add inside the layer fails for repositories in the project.
  • Base protection: it is still best effort (paths built at run time are not seen; isolation is the boundary). It now refuses some very large calls instead of letting them through. With it on, every Read, Grep, Glob and LS costs one hook round trip.
  • Plugin data under base protection: an agent outside the layer can no longer read plugin data by path except its own account home and a project placed there; e.g. a Manual orchestrator reading a subagent's worktree under plugin data by path is refused (the change review goes through the plugin). A plugin that sets CLAUDE_CONFIG_DIR to its whole data folder still reopens it for that card (pre-existing agentRoots behaviour).
  • Hermes: a busy rollback-journal Hermes database can delay one usage poll by up to about 7 s.

Depends on

#147 (pr/orchestration-reliability), for text conflicts only. Merge it first. Until then, this PR also shows its 9 commits.

…h files

Closing a card started the run cleanup and the removal of runs/<id> at the same time, and a launch still being prepared (plugin prepare, environment wrap) could write run files after the folder was gone. A throw from the environment wrap also skipped the planned and contributed cleanup.

Contributed launches are now tracked. Closing waits for pending launches, then the serialized exit/restart cleanups, then removes the run folders each pipeline created for this card object, and the card folder only when it is empty, so a card brought back under the same id keeps its new run. An unadopted launch releases its planned spawn and contribution in a finally block, and a launch whose card was closed or restarted in the same tick never asks its plugins. Run folders are removed with retries for files Windows still holds open.
If closing the Even G2 companion threw, quitting skipped terminalManager.shutdown(), so PTYs were not hung up and sessions were not saved. Shutdown now runs its steps in order through runShutdownSteps, which logs a failed step and continues.
…e off

A Codex or OpenCode subagent's final answer could belong to an earlier
turn: OpenCode reused its session id as the turn id and reported idle
before its asynchronous answer read finished, and recordAnswer accepted
any answer. OpenCode now names each turn, reports no idle before the
answer is read and drops a read a newer turn superseded; retry counts as
a turn start. A submitted input (not an approval answer, not an empty
Enter in an idle card) starts a new task, and only an answer of the turn
that answers it is kept. A signal of that turn which predates the input
still moves the status, so wait_for_agent ends instead of hanging, but
brings no answer. An answer reported as the agent exits still counts.
main's signal glue is now applyRuntimeSignal, shared with the tests.

With CanvasTTY status hooks off a launch gets no hooks, so no answer can
be captured. Whether a launch has them is decided when it is planned
(deferred plugin launches included). wait_for_agent and get_agent_result
then return answerUnavailable { reason: "status_hooks_off", message } so
the orchestrator reads the output instead.

Every write of person or agent input now goes through one method,
writeInput, which does this bookkeeping; input() and deliverInput call it,
and a later writer that holds input and replays it must call it too, so a
replayed Enter counts as a submitted prompt. It writes nothing into a
process the card no longer runs.
…UseFailure

Once a turn id was active, the gateway dropped every non-start event with
a new turn id. Each hook runs on its own connection and can time out, so
when a turn's start hook was lost the whole turn, including its Stop, was
ignored and the card kept the previous turn's status. The gateway now
tracks whether a turn is open. After an idle, an unseen turn id opens the
next turn; during an open turn, or for an id already seen, the event is
still dropped as out of order.

When a tool the person approved fails, the agent CLI runs
PostToolUseFailure instead of PostToolUse. CanvasTTY did not register it,
so the card stayed needs_approval until the next hook although the turn
went on. It now maps to working.
node-pty reports a process killed by a signal as exitCode 0 plus the
signal. The signal was dropped, so an agent killed with SIGKILL (OOM,
kill -9) was recorded as "done, exit 0": wait_for_agent returned done,
get_agent_result said done, and the saved session restored as exited.

Record such an exit the way a shell does, as 128+signal (SIGKILL -> 137),
mark the session failed, and put the signal name first in the failure
details. Every reader of the exit (wait_for_agent, get_agent_result, the
session store, the HOME failure summary) already derives its state from
the exit code, so they now agree. Exits CanvasTTY causes itself (closing a
card, quitting) are unaffected: those sessions are removed before
the kill, and the exit handler ignores them.
A subagent spawned without launch options ran on its CLI's own default
model and sign-in (for OpenCode the hosted OpenCode Zen model), even when
its orchestrator ran on a model the person picked through a plugin's
launch options. A plugin the spawn does not name now keeps the
orchestrator's own values for a subagent of the same CLI; repeating the
person's choice needs no delegable declaration, and then the normalized
values must equal the orchestrator's exactly (missing keys come from the
orchestrator, never from the plugin's defaults). A subagent of another
CLI must name a plugin whose contribution set the orchestrator's model,
and spawn_agent reports servedBy (CLI, requested model, inherited or
explicit options).

- Recognise every form a CLI takes for its model: --model and --model=,
  -m and -mX for OpenCode, Codex, Qwen, Kimi and Grok, and for Codex also
  --profile/-p and -c/--config setting model, model_provider or profile.
- Refuse a second model (an explicit model next to a plugin that sets it,
  or two plugins that both set it; OpenCode crashes at start on two
  --model values) only for a subagent's launch. A person's own launches
  and restores start as before.
- Inherit an empty choice (a plugin without fields) as chosen. Leave out
  an inherited plugin that was turned off since, unless it set the model
  (then refuse); refuse with a clear reason when its fields changed.
- Reset modelSetBy at the start of each launch.
- Describe inheritance, servedBy and the refusals in the orchestrator
  skill and docs.

The core stays plugin-agnostic: it reads the plugin from the launch
options and the contribution, never from a fixed plugin id.
OpenCode drops every key that arrives while its full-screen view starts
(a few seconds). CanvasTTY typed spawn_agent's first prompt, send_to_agent
and plugin sessions.send as soon as the PTY existed, so an OpenCode
subagent sat at an empty prompt until the orchestrator gave up.

deliverInput now waits, within the same bounded wait it already had for
the launch, until OpenCode can take input: the terminal control sequence
that shows its text cursor on the alternate screen once its prompt has
focus, or a lifecycle hook from its plugin, counted only 2 s after the
launch's first one (the plugin can report before the prompt takes keys).
Only DEC private mode sequences are read; human-readable screen text is
never parsed and no status changes. Other CLIs keep getting input when
the PTY exists.

- The wait is used on macOS and Linux. Windows' pseudo console may not
  pass those sequences, so there input is typed when the PTY exists, as
  before.
- spawn_agent (checks, launch and readiness together) and send_to_agent
  wait at most 45 s, so the orchestrator gets the reason and the session
  id, not an MCP client timeout. Nothing is typed then.
- A handoff waits up to 15 s for the CLI to take input instead of
  failing at once; a caller that does not wait is told the state.
- Sends to one card are delivered in the order they were made, a queued
  send never reaches a later launch, and a send cancelled while it waits
  behind another settles at once and types nothing.
wait_for_agent accepted up to 600 seconds, but the MCP clients of several
supported CLIs (OpenCode among them) end a tool call after about 60
seconds, so an orchestrator that asked for a long wait got a failed call
instead of a result.

A call now asks for at most 100 seconds (default 50). Unless the
orchestrator's CLI is known to let a tool call run longer, a call returns
after at most 50 seconds. A timeout carries the status and the masked
output as progress plus a message to call again, and the tool
description in both catalogs, the orchestrator skill and the docs say
that a timeout means the subagent is still running.
Base protection is documented to refuse use of CanvasTTY's private data,
but its hooks only saw shells and file writes: Claude Code's Read, Grep,
Glob or LS, and the Qwen Code and OpenCode equivalents, could read
provider-secrets.bin, the control token or another account's home
without a check.

While base protection is on at launch, the decision hook now also
matches those read-only tools (Claude Code and Qwen Code matchers,
OpenCode's guard through CANVASTTY_RUNTIME_DECISION_READS). With base
protection off, a decision plugin's fail-closed hook still covers only
shells and writes, so reads never wait on the gateway. They map to a
read action that is checked only against the private data, with the
same rules as a shell read: the agent's own account home stays its own,
a search walks its folder, and nothing outside the private data is
refused or asked about. Decision services are not asked about reads.

OpenCode's grep without a path is sent as a search of ".", and glob's
pattern is always sent. Qwen Code's read_many_files is refused when its
paths and include globs together exceed 32, instead of checking only
the first 32, and include globs are checked. A cut read-only call is
refused like a cut shell call. Codex has no read-only file tool in its
hooks; it reads through its shell, which is already checked.

The docs now say base protection is best effort: it does not see paths
built at run time or folder walks started above the app's data, so a
project at the home folder needs agent isolation to keep the data
private.
The private-data rule matched globs one component at a time, so `**`
stood for a single folder and `cat ~/Library/**/provider-secrets.bin`
was allowed; brace expansion was not seen at all, so
`cat <userData>/provider-secrets.{bin,}` was allowed too.

- `**` now matches zero or more folders. A glob that only passes by the
  app's folder on its way to other names (`~/**/*.ts`) is not counted.
- Unquoted braces in a shell word, and braces in a Glob tool pattern,
  are expanded (at most 64 words) before matching.
- On macOS and Windows a glob matches private names in any letter case.

Base protection also had limits that let the rest of a call through
unchecked. A call now is refused ("uncheckable") instead when:

- a path field is not a string, or is longer than 4096 characters after
  folding `./` and `a/..` padding;
- read_many_files names more than 32 paths and include globs, or an
  apply_patch touches more than 256 files or a too-long path;
- the private-path budget (now 1024 checks), the brace-expansion limit,
  the path candidates in one word or heredoc (256), or the nesting of
  shells and substitutions runs out;
- checking a shell, file-edit or read call fails with an error. Before,
  an exception in the analyzer let the call through; other tools still
  get no opinion.

An apply_patch whose command is longer than 4096 characters is read in
full; before, it was treated as touching no file.
bubblewrap bound the shared gateway socket folders writable into every
isolated agent, so an agent could replace a gateway socket and receive
other agents' requests. It also took a socket folder from dirname() of
every CANVASTTY_*_ADDRESS in the launch environment, which holds the
person's variables and plugin contributions: $HOME/foo.sock bound all of
HOME after the tmpfs masks, and an address inside plugin data reopened
every account home. The control grant folder was read from the same
environment.

Socket folders are now bound read-only (connect() needs no write access
to the folder) and come only from the sockets CanvasTTY's own gateways
prepared for the launch; the control grant only from the control
environment the host gave an orchestrator. A socket that must not be
read is masked with /dev/null, and a gateway socket folder that holds a
hidden path is refused. On macOS the seatbelt Unix-socket allow list is
built from the same gateway sockets.
… them

With a project that contains userData (the home folder), an orchestrator
could start a subagent in <userData>/plugin-data/<plugin>/homes. The
isolation layer reopened any project at least two folders deep in plugin
data, so that subagent could read and write every account's sign-in.

A subagent's folder may no longer be inside CanvasTTY's private data,
unless the project itself is a folder inside that same plugin's data (an
environment's worktree). The layer reopens a project inside plugin data
only when the host placed the launch there: a folder an environment
prepared inside its own plugin's data, or the folder the person chose for
the card the launch descends from. Anything else is refused with the
reason.

Also: a gateway socket folder that contains a writable folder is refused
instead of mounted over it; a trailing separator in a CLI home variable
names the same account home; the docs list which variables count as an
account home, in English, Russian and Chinese.
…ct that contains it

When an isolated agent's project contains CanvasTTY's userData folder (a
project at the home folder), the whole project was writable, so the app's
settings.json and the plugins folder were writable to the agent; only the
private parts were hidden.

The isolation paths now carry readOnlyFolders (userData when a writable
folder contains it) and fixedFolders (its parents inside that folder).
Seatbelt denies writes in userData after the project allow, re-allows the
writable folders inside it (a selected account home) and denies renaming
or replacing the parents. bubblewrap binds each parent onto itself (a mount
point cannot be renamed), mounts userData read-only over the project and
binds the writable folders inside it again. A project located inside
userData is unaffected.
…worktrees

Inside the isolation layer only a repository's hooks, info/attributes and
an existing .git/config were protected. .git/commondir (where git reads the
shared config and hooks from), .git/config.worktree and the linked
worktrees' records in .git/worktrees stayed writable, and the path-based
rules could be sidestepped by renaming .git, .git/hooks or .git/info away
and back.

Seatbelt now denies writes to commondir, config.worktree and worktrees/ in
any repository of the project, existing or not, and denies renaming or
removing .git, .git/hooks and .git/info. For an existing repository the
paths list config.worktree and worktrees/ (Linux mounts them read-only,
with neutral placeholders when missing) and an existing commondir; on
Linux the project's .git is bound onto itself so it cannot be renamed.
Objects, refs, the index, logs and HEAD stay writable.
Base protection treated anything inside the working folder (and the
agent's config folder) as the agent's own before it checked
CanvasTTY's private data. With the project at the home folder, or at
any folder that holds the app's userData, an agent's shell command or
file tool could read or overwrite provider-secrets.bin, the control
token, other account homes and the gateway records. A project in a
temporary folder likewise reopened the gateways' socket folders.

A folder is now the agent's own only below the private data: a
private path or socket folder inside the project or config folder
stays private. A folder inside a private path (the account home the
agent was launched with) is still its own, and its neighbours are not.
A recursive walk of a folder in the app's data that holds private
paths is refused wherever the project is.

The per-card Claude Code draft folders (<userData>/claude-draft) are
listed as private data too; nothing creates them yet.
… immutable

A database without -wal/-shm was always opened with immutable=1, which skips locking and hot-journal checks, so a rollback-journal database could be read while Hermes was writing it. Use the immutable read only when the header says WAL (bytes 18 and 19 are 2) and no side files exist; open other databases with mode=ro and retry SQLITE_BUSY a bounded number of times before skipping the poll.
…edential rules

Held values were masked leftmost-first and the scan jumped past each match, so a second held value overlapping the first showed its suffix. Masking held values before the generic rules could also erase a label such as password or Authorization that a rule needed, leaving the credential visible, and each call allocated text-sized typed arrays. Find held values, JSON values and credential shapes as source ranges on the original text and mask the union of overlapping ranges; search held values with indexOf/KMP streams and a ring buffer instead of text-sized arrays; count entropy characters without regex match arrays. The empty-registry path is unchanged.
…ere read

The Files reader and the usage history reader bind an opened descriptor to
its path by dev/ino. As numbers, 64-bit file ids (NTFS, some network file
systems) lose precision, so two different files could compare equal.
Compare bigint stats instead.

The history reader also compares the pre-open lstat with the descriptor, so
a file swapped between the symlink check and the open is refused rather
than read and then quarantined. When invalid records are dropped, the
original is preserved from the bytes read through the checked handle
(exclusive create, mode 0600) instead of a second copy by path.
wait_for_agent, get_agent_result and observe_agent returned the raw PTY
tail, which for a full-screen TUI such as OpenCode is cursor moves,
colours and spinner redraws, often starting mid-sequence. They now render
the masked scrollback window (the last 65,536 characters) through the
existing headless xterm at the session's size and return the visible
text, masked again and capped as before (observe_agent within its
maxChars). A cut window starts at its first escape or line break, so the
rest of a cut sequence ("18;9H") is never printed as text.

exitLines were the regex-stripped last 16 KiB, so a TUI that redraws only
the cells that changed read as its redraws joined together, and a line
drawn early and never redrawn (the model's status line) was lost. They
are now rendered by the same headless terminal from the same window,
masked before and after, and the three tools return the same exitLines.
An isolated agent could read every plugin's private data folder
(<userData>/plugin-data), including every CLI home and token an
accounts plugin keeps there, not only the account it runs on.

plugin-data is now private app data for the isolation layer. The one
folder reopened is the CLI home a chosen launch contributor started the
agent with: the provider's own home variable set to an exact absolute
folder strictly inside that contributor's host-assigned data folder.
The host derives it from the contribution; a plugin cannot name a grant.
AgentIsolation checks it again when the layer is built (real folders,
no symbolic links from plugin-data down) and refuses the launch
otherwise, giving the reason once. A project inside plugin data is
reopened as that one folder.
Claude Code keeps its Bash tool output and external-editor files in
${CLAUDE_CODE_TMPDIR:-/tmp}/claude-<uid> and ignores TMPDIR. The isolation
layer does not make /tmp/claude-<uid> writable, so every shell call of an
isolated Claude failed with "Operation not permitted".

Set CLAUDE_CODE_TMPDIR to the launch's private temp folder for Claude on
both layers (seatbelt and bubblewrap). The shared /tmp/claude-<uid> is not
granted: all of the person's Claude sessions use it, and an isolated agent
could otherwise rewrite another session's task output.
…card's own account home

An accounts plugin keeps every account's sign-in in CanvasTTY's plugin data
folder. Base protection's private list had account-homes but not
plugin-data, so an agent outside the isolation layer (a person-launched
card, or one with isolation off) could read another account's tokens.

Add plugin-data to the private list. The account home the card's launch
plugin selected (the home the launch pipeline already validates for the
isolation layer) is recorded with the card and counts as one of its own
config folders, as CLAUDE_CONFIG_DIR does, but only while the CLI is still
started with it. Other accounts' homes, other plugins' data and the
listing stay refused; a project CanvasTTY placed inside plugin data keeps
its own files. plugin-data is not added as a name marker, since such a
project names that folder in its own paths.
@BIackFIame

Copy link
Copy Markdown
Contributor Author

Reworked as #161, because it now builds on the reworked orchestration PR (#160) and on #159. #159 adds its own check for a plugin-provided account home, and the new PR merges the two into one rule, so the home is checked only once. The 13 fixes are the same and already shipped in v1.8.2-test.2; the new PR brings them to main. Closing this one in its favour. Thank you.

@BIackFIame BIackFIame closed this Oct 22, 2026
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