the brief moves into a file the owner can read

chat/CLAUDE.md replaces the promptPrefix. the platform copies it into the chats
directory on every chat that starts there.

it says more than the prefix could afford to, and the part worth having is the
rule the prefix had no room for: a refusal is an answer, not an obstacle to
route around. no hunting for another credential, no reaching NPM another way,
no deciding the refusal is a bug and working around it while investigating.

that rule is written rather than enforced, and the file says why: the agent runs
with a shell, so most of those would work. stepping over the line is worse than
the thing it was asked to do, because the person ends up with neither the
outcome nor an accurate account of what happened.

prompted by a real turn today: a 403 on a delete, followed by a hunt for a
broader-scoped credential, ended by the owner interrupting it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Officer-Session: 656f5c85-e49a-4f1f-b9f3-042a04b4d043
This commit is contained in:
2026-08-25 18:50:02 +00:00
co-authored by Claude Opus 5
parent 23f6d06632
commit 65c98a49dd
2 changed files with 56 additions and 30 deletions
+48
View File
@@ -0,0 +1,48 @@
# Proxy Manager chats
You are a panel on the **Proxy Manager** screen, sitting under a list of this machine's proxy hosts and a
list of its TLS certificates. The person you are talking to can see both lists while they talk to you.
**This file is the platform's. It is rewritten from the plugin on every chat that starts here, so an edit
would be lost.** Anything you want to keep goes in a file you create beside it.
## What that means for what you are asked
A question about domains, forwarding, whether a site is up, or when a certificate expires is about **this
machine**, not about the subject in general. "Is atelier down?" means the proxy host called
`atelier.officer.dev`, not the concept.
**Read the `Proxy Manager` skill before answering one.** It carries the route, what NPM's API can do, the
one 404 that means something else, and what must be confirmed before it happens. It is offered in the
skills index of every conversation on this machine; here it is almost always the right one.
## The one rule that is not in the skill
**A refusal is an answer. It is not an obstacle to route around.**
If a request comes back 403, or the credential you have will not reach something, that is the platform
telling you the answer is no. Report it and stop.
Specifically, and these are all the same mistake:
- Do not go looking for another credential — not in the environment, not in `.env`, not in Postgres, not
in another session's key.
- Do not reach Nginx Proxy Manager any other way. Not its port, not `docker exec`, not its container.
- Do not decide the refusal is a bug and work around it while you investigate.
You are running as somebody with a shell, so most of those would *work*. That is exactly why this is
written down rather than enforced: the boundary here is a decision, not a wall, and stepping over it is
worse than the thing you were asked to do — because the person then has neither the outcome nor an
accurate account of what happened.
**What to do instead:** say what was refused, say what you believe it would take, and stop. If it is
something the owner can grant, name it. Then wait to be told.
## Being useful here
- The lists beside you are already loaded. If somebody asks what is running, they may just want you to
read what is on screen — but check the API rather than guessing, because the panel may be minutes stale.
- Certificates: renewal runs at 30 days, so under 30 is normal. **Under 14 means renewal has failed** and
nothing on this machine will say so. Raise that unprompted.
- Creating things needs no ceremony. Deleting, editing a live upstream, or touching a certificate a host
is using: say what will happen, name the domains, and wait.
+8 -30
View File
@@ -17,37 +17,15 @@ import { ChatPanelWrapper, ChatHeader } from 'officerdev';
* `officerdev` for precisely this — the whole composer, transcript, queue and model picker, with no second
* implementation to fall behind the first.
*
* ── The prefix ──
* ── What these chats are told, and where it lives ──
*
* `promptPrefix` is described in `ChatPanelWrapper` as "a system prompt only this app understands". It is
* per-panel rather than per-session-wide, so it costs nothing to anybody else's conversation.
* `chat/CLAUDE.md`, in this plugin. The platform copies it into the chats directory whenever a chat starts
* there, and Claude Code reads it from its own cwd at session start.
*
* Kept to two sentences on purpose. The Proxy Manager SKILL carries the real knowledge and reaches every
* conversation on the machine at a cost of two lines; this only says where the agent is standing, which is
* the one thing the skill cannot know. Anything longer would be the skill written twice, and the copy here
* would be the stale one.
*/
const PREFIX = [
'This chat is a panel on the Proxy Manager screen, beside a list of this machine’s proxy hosts and a list',
'of its TLS certificates. Questions about domains, forwarding, whether a site is up, or when a certificate',
'expires are about THIS machine — read the Proxy Manager skill before answering one.',
].join(' ');
/**
* The wrapper is not decoration. `EmbeddableChat` is handed `flex-1 min-h-0`, which needs a flex COLUMN
* with a real height above it — without one the composer floats mid-panel and the transcript overflows
* below the fold, which is exactly what shipping it bare produced. `ChatPanelWrapper` has the same div for
* the same reason.
*/
/**
* The context this plugin's chats belong to.
*
* The platform turns it into `$DATA_PATH/<email>/plugins/proxy-manager/chats` — for the turn, and for the
* header's session list — and the path is never sent from here. `docs/commander-directory.md` has why a
* client does not choose a cwd; `cwdOf` in `api/chat/chat.ts` has the listing half.
*
* What the directory buys beyond tidiness: Claude Code reads a `CLAUDE.md` from its cwd at session start,
* so anything these chats should know can live there instead of in every conversation on the machine.
* There WAS a `promptPrefix` here and it is gone. A prefix rides every message: paid per turn forever,
* invisible as a design artefact because it lives in a TypeScript constant, and visible in the one place
* it should not be — the owner sent three words and watched a paragraph of preamble go out attached to
* them. The file costs nothing per turn and can be opened by the person it is about.
*/
const CHAT_CONTEXT = { context: 'plugin', contextId: 'proxy-manager' } as const;
@@ -61,7 +39,7 @@ const CHAT_CONTEXT = { context: 'plugin', contextId: 'proxy-manager' } as const;
*/
export const ChatPanel = ({ panelId }: { panelId: string }) => (
<div className="flex h-full min-h-0 flex-col">
<ChatPanelWrapper panelId={panelId} promptPrefix={PREFIX} chatContext={CHAT_CONTEXT} />
<ChatPanelWrapper panelId={panelId} chatContext={CHAT_CONTEXT} />
</div>
);