Compare commits

..
10 Commits
Author SHA1 Message Date
pastilhas dc91f78f4f officer.dev: the restart rule was stale, and said the opposite
This file said "NEVER run pm2 yourself — tell the owner the exact command." The
owner granted the opposite on this machine on 2026-08-19, so the document and
the instruction disagreed for a day, and user/CLAUDE.md already flagged it as a
local rule to follow "while it says so".

It now says to run the restart and report it. The scope is unchanged and is the
part that mattered: only the process the change needs, never the whole
ecosystem.config.cjs.

Added the caveat the grant did not remove — check who else is live first. The
owner edits the platform using the platform, several sessions share one
checkout, and a restart is the moment a half-applied change from someone else's
work in progress would ship.
2026-08-21 00:24:29 +00:00
pastilhas eec6a4962e install.sh: clone anywhere, run one command
A manifest declares repo path -> target, and the script walks it. Symlinks, not
copies, so an installed file edits the repo and one commit reaches every other
machine. Idempotent — a correct link is a no-op, safe after every pull. A real
file where a link should go is moved to .bak-<timestamp> and reported, because
on a new machine that may be the only copy of something written by hand.

Conditional targets are what make it portable: officer.dev/CLAUDE.md and the
officer-chat-events hook both name ~/officer-dev/platform, so a laptop with no
Officer install gets neither instead of getting a document that lies.

mask-secret is a command now rather than a path only true on this machine, and
user/CLAUDE.md says so. It is a generated wrapper, not a symlink, and that is
forced: mask-secret.py resolves its own path to find mask-hooks.d, so a symlink
would reach back into the repo and run every machine's hooks. The wrapper sets
MASK_HOOKS_DIR to the ones this machine installed and still runs the script from
the repo, so no code is copied.

Verified against a scratch HOME: re-run is all ok, the hand-written file
survives as .bak, and a machine without Officer skips both conditional lines.
2026-08-21 00:24:22 +00:00
pastilhas 63ab090bb2 never suggest stopping 2026-08-19 07:54:12 +00:00
Claude c21a0725cb user: how to actually show him an image
Asked three times on 2026-08-19 to see a generated icon and answered with a
Preview window, a hunt for terminal image tools, and a hyperlink before an image
appeared. Nothing said he reads through a markdown client, or that the file has
to be public before a tag can reach it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 05:58:31 +00:00
pastilhas 4036f78cc3 note: reference only, not adopting mcp 2026-08-13 02:37:40 +00:00
pastilhas 3e2a997e58 note ai-memory as prior art for tomorrow 2026-08-13 02:35:33 +00:00
pastilhas 82f9907d26 decide tools install to ~/.local/claude-scripts 2026-08-13 02:13:50 +00:00
pastilhas ed925a14b8 note the bootstrap installer design for next session 2026-08-13 02:10:07 +00:00
pastilhas 28d58728cf split machine-specific stores into mask hooks 2026-08-13 02:08:18 +00:00
pastilhas c6cb958ba5 do not mask throwaway credentials 2026-08-13 02:05:51 +00:00
8 changed files with 413 additions and 47 deletions
+53
View File
@@ -1,3 +1,56 @@
# claude-mds
CLAUDE.md files and Claude Code configuration, kept under version control.
## Install, on any machine
```sh
git clone git@gitea.officer.dev:pastilhas/claude-mds.git ~/projects/claude-mds
~/projects/claude-mds/install.sh
```
`--dry-run` first if you want to see it before it happens.
Everything is **symlinked, not copied** — so editing an installed file edits the repo, and the change
is one `git commit` away from every other machine. Re-run it after each `git pull`; a correct link is
a no-op.
It will not overwrite anything in silence. A real file where a link should go is moved to
`<name>.bak-<timestamp>` and reported, because on a new machine that file may be the only copy of
something written by hand.
## What lands where
`manifest` is the whole answer, and it is the only place paths are declared. Today:
| repo | target | only if |
| ---- | ------ | ------- |
| `user/CLAUDE.md` | `~/.claude/CLAUDE.md` | always |
| `officer.dev/CLAUDE.md` | `~/officer-dev/CLAUDE.md` | `~/officer-dev/platform` exists |
| `tools/mask-hooks.d/officer-chat-events` | `~/.local/claude-scripts/mask-hooks.d/` | `~/officer-dev/platform` exists |
Plus `mask-secret` on `~/.local/bin`, as a command rather than a path.
Adding a file later is one manifest line, not an edit to the shell.
## The two kinds of file
- **`user/`** — who you are working with and how. Loads in every session on every machine.
- **`officer.dev/`** — one workspace on one machine. Conditional, so it never lands where it would be
a lie.
## mask-secret
Masks a pasted secret everywhere the chat history keeps it:
```sh
printf '%s' "$SECRET" | mask-secret
```
It owns Claude Code's own transcripts. Anywhere else that keeps a second copy of the chat — a
database, a mirror, a log shipper — is a **hook**: an executable in `tools/mask-hooks.d/`, listed in
the manifest with the condition that says which machines it belongs on.
The installed `mask-secret` is a small generated wrapper, and that is deliberate. The script finds its
hooks beside itself and resolves symlinks to do it, so a bare symlink would reach back into the repo
and run *every* machine's hooks. The wrapper points it at the ones this machine installed.
+78
View File
@@ -0,0 +1,78 @@
# TODO
## Bootstrap installer — BUILT 2026-08-20
`install.sh` plus `manifest`. Clone the repo, run one command, every file lands where the tooling
expects it. See README.md.
Everything the plan asked for is in: a manifest rather than paths in shell, symlinks rather than
copies, idempotent, conditional targets, backups instead of silent clobber, `--dry-run`.
Verified on this machine and against a scratch `HOME`: a re-run is all `ok`, a real file in the way is
moved to `.bak-<timestamp>` and survives, and a machine with no `~/officer-dev/platform` skips both
the workspace file and its hook.
**`mask-secret` is a command now**, on `~/.local/bin`, and `user/CLAUDE.md` names it instead of the
hardcoded path that was only ever true here. It is a generated wrapper, not a symlink, and that is
forced: `mask-secret.py` finds hooks at `Path(__file__).resolve().parent/mask-hooks.d`, and `.resolve()`
follows a symlink back into the repo — which carries every machine's hooks, not this one's. The
wrapper sets `MASK_HOOKS_DIR` and still executes the script from the repo, so no code is copied.
### Still open
- **Bootstrapping the bootstrap.** The machine needs git, an SSH key for Gitea, and the repo URL
before any of this runs. A one-line `curl | sh` would close it, and may be deliberately not worth
having.
- **`settings.json` and hooks.** Not managed here yet. The repo is named for the docs but already
carries `tools/`, so the line is already crossed; the question is whether per-machine settings can
be expressed as one file plus conditions, or whether they are genuinely per-machine.
- **Which files are universal.** `officer.dev/` is the only machine-specific one so far. The
`platform-appstore/` set is a candidate and was never folded in.
- **PATH.** `install.sh` warns when `~/.local/bin` is missing from `PATH` but does not edit a shell
profile. Editing someone's `.zshrc` from an installer is a bigger intrusion than the warning.
### Prior art to discuss: akitaonrails/ai-memory
<https://github.com/akitaonrails/ai-memory> — read 2026-08-13. Solves a superset
of this: cross-vendor long-term memory so you can quit Claude Code mid-task and
continue in Codex without re-explaining. A Rust binary plus a data dir, served
over MCP, with `install-mcp` and `install-hooks` subcommands.
Worth stealing even if we never run it:
- **`_global` scope.** A reserved project that is unioned into every query —
standing personal rules, tech choices, style. Exactly what `user/CLAUDE.md`
is. Confirms the split we already made.
- **Page categories**: `_rules/`, `decisions/`, `procedures/`, `gotchas/`.
Better than one growing file. Our `user/CLAUDE.md` is six sections and will
not stay readable at twenty.
- **Authority-aware retrieval.** Maintained rules outrank episodic session
evidence, but do not become absolute. A good answer to "the doc says X but
the code does Y" — which bit us today with the stale `pm2` rule.
- **`.ai-memory.toml` marker file** in a repo ancestor to pin which
workspace/project a directory belongs to. Directly relevant to our manifest's
"only if this path exists" condition — a marker is more explicit than a path
test.
- **`bootstrap`** seeds pages from `git log`, README, `docs/`, module headers.
The thing we would want when adding a new machine or repo.
**His position, stated up front: not a fan of MCP, and never was.** So this is a
source of ideas, not a thing to adopt. Do not open tomorrow by proposing we
install it. Harvest the taxonomy and the concepts, build our own on plain files.
The real decision: it is a **running service** (daemon, SQLite, FTS5,
embeddings, MCP, hooks). Ours is plain files and symlinks, readable with `cat`,
and works when nothing is installed. That simplicity is a feature, especially
given today's "code on disk is not what is running here" problem. Options:
adopt it, borrow the taxonomy only, or stay flat and revisit when the flat file
gets unreadable.
### Open questions
- Which files are genuinely universal vs this-machine-only? Right now
`officer.dev/CLAUDE.md` is the only machine-specific one, but the
`platform-appstore/` set is a candidate and was never folded in.
- Should `settings.json` / hooks be managed here too, or only `.md` files? The
repo is named for the docs but already carries `tools/`.
- Bootstrapping the bootstrap: the machine needs git and the repo URL before any
of this runs. Worth a one-line `curl | sh`, or deliberately not.
Executable
+99
View File
@@ -0,0 +1,99 @@
#!/usr/bin/env bash
#
# Put every file in this repo where the tooling expects it, on any machine.
#
# git clone git@gitea.officer.dev:pastilhas/claude-mds.git ~/projects/claude-mds
# ~/projects/claude-mds/install.sh
#
# Symlinks, never copies. Two copies drift, and the point of the repo is that an edit lands in git.
# Safe to re-run after every pull: a correct link is a no-op. A real file in the way is backed up,
# never overwritten in silence.
#
# --dry-run say what would happen, change nothing
#
set -euo pipefail
REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
MANIFEST="$REPO/manifest"
CLAUDE_SCRIPTS="${CLAUDE_SCRIPTS:-$HOME/.local/claude-scripts}"
HOOKS_DIR="$CLAUDE_SCRIPTS/mask-hooks.d"
BIN_DIR="${BIN_DIR:-$HOME/.local/bin}"
STAMP="$(date +%Y%m%d-%H%M%S)"
DRY=0
[[ ${1:-} == --dry-run ]] && DRY=1
say() { printf ' %-7s %s\n' "$1" "$2"; }
run() { (( DRY )) || "$@"; }
# Symlink $2 -> $1, refusing to destroy anything that is not already our link.
link() {
local src=$1 dst=$2
if [[ ! -e $src ]]; then say "MISSING" "$src — not in the repo"; return 1; fi
if [[ -L $dst && "$(readlink -f "$dst")" == "$(readlink -f "$src")" ]]; then
say "ok" "$dst"; return 0
fi
# A real file, or a link somewhere else. It may be the only copy of something hand-written here.
if [[ -e $dst || -L $dst ]]; then
say "backup" "$dst -> $dst.bak-$STAMP"
run mv "$dst" "$dst.bak-$STAMP"
fi
run mkdir -p "$(dirname "$dst")"
run ln -sfn "$src" "$dst"
say "link" "$dst"
}
# `mask-secret` as a COMMAND, not a path — so user/CLAUDE.md can name it and be true everywhere.
#
# A wrapper rather than a symlink, and that is forced: mask-secret.py finds its hooks at
# `Path(__file__).resolve().parent/mask-hooks.d`, and .resolve() follows a symlink back into the repo
# — which holds EVERY machine's hooks. The wrapper points MASK_HOOKS_DIR at the ones this machine
# actually installed, and still executes the script from the repo, so the code is never copied.
install_command() {
local script="$REPO/tools/mask-secret.py"
[[ -e $script ]] || { say "MISSING" "$script"; return 1; }
run mkdir -p "$BIN_DIR" "$HOOKS_DIR"
if (( ! DRY )); then
cat > "$BIN_DIR/mask-secret" <<WRAPPER
#!/bin/sh
# Generated by claude-mds/install.sh — edit install.sh, not this.
export MASK_HOOKS_DIR="$HOOKS_DIR"
exec "$script" "\$@"
WRAPPER
chmod +x "$BIN_DIR/mask-secret"
fi
say "cmd" "$BIN_DIR/mask-secret"
}
main() {
(( DRY )) && echo "dry run — nothing will change"
echo "repo: $REPO"
echo
local kind src dst cond
while read -r kind src dst cond || [[ -n ${kind:-} ]]; do
[[ -z ${kind:-} || $kind == \#* ]] && continue
# A `hook` line has no target column: its destination is derived, so the condition shifts left.
if [[ $kind == hook ]]; then cond=$dst; dst="$HOOKS_DIR/$(basename "$src")"; fi
dst=${dst/#\~/$HOME}
cond=${cond:-}; cond=${cond/#\~/$HOME}
if [[ -n $cond && ! -e $cond ]]; then
say "skip" "$dst — no $cond on this machine"
continue
fi
link "$REPO/$src" "$dst" || true
done < "$MANIFEST"
echo
install_command
case ":$PATH:" in
*":$BIN_DIR:"*) ;;
*) echo; echo " note $BIN_DIR is not on PATH — add it, or mask-secret is not a command yet" ;;
esac
}
main "$@"
+16
View File
@@ -0,0 +1,16 @@
# What goes where. One line per file; install.sh walks this and nothing else.
#
# kind source (in this repo) target condition
#
# kind link — symlink target -> source. The file lives in git; the edit lands in git.
# hook — a mask-secret hook. Target is derived: $CLAUDE_SCRIPTS/mask-hooks.d/<name>.
#
# condition — optional. A path that must exist on this machine, or the line is skipped.
# This is what keeps officer.dev's workspace file off a laptop that has no Officer.
#
# ~ means $HOME. Columns are whitespace-separated, so no spaces in paths.
link user/CLAUDE.md ~/.claude/CLAUDE.md
link officer.dev/CLAUDE.md ~/officer-dev/CLAUDE.md ~/officer-dev/platform
hook tools/mask-hooks.d/officer-chat-events ~/officer-dev/platform
+10 -4
View File
@@ -28,10 +28,16 @@ This file captures the **cross-cutting operational reality** that neither of tho
After editing anything under `platform/src` (backend *or* frontend), it needs a
**`pm2 restart officer`** to take effect. Forgetting this looks exactly like "my fix didn't
work." Confirm the reload before diagnosing further.
- **NEVER run pm2 yourself — tell the owner the exact command to run.** After a platform change,
print the precise `pm2 restart <process>` command that reloads ONLY the process(es) the change
needs (for platform TS edits that is `pm2 restart officer` alone — never the whole
`ecosystem.config.cjs`, which bounces every sidecar). The owner runs it.
- **Run the restart yourself, and say that you did.** This said "NEVER run pm2 yourself" until
2026-08-20. The owner granted the opposite on this machine: restart when a change needs it rather
than handing over a command to paste. What did NOT change is the scope — restart ONLY the
process(es) the change needs (for platform TS edits that is `pm2 restart officer` alone, never the
whole `ecosystem.config.cjs`, which bounces every sidecar).
- **Check who else is live first.** The owner edits the platform *using* the platform, and often has
several sessions open, so a restart can cut another agent off mid-turn or bounce the process you
are both running inside. Look at `pm2 list`, the session list, the process table. Restarting is
also the one moment a half-applied change from a shared checkout ships — confirm the tree is
coherent (typecheck clean, database schema matching the code) before you bounce anything.
- **Capabilities ARE hot — no restart.** Files under `capabilities/` are re-read from disk on each
run / request (Pi per agent session, the app per request). Edit `TASK.md` / `index.ts` /
`categories.yaml` and it's live immediately. This asymmetry is the #1 source of confusion:
+66
View File
@@ -0,0 +1,66 @@
#!/usr/bin/env python3
"""Officer platform: mask a secret in `chat_session_events`.
SPECIFIC TO THIS MACHINE. Officer's sidecar commits every turn — assistant text,
tool inputs and tool outputs — to Postgres, so the web chat can replay a session
after a restart. That makes it a second readable copy of the conversation, and
the one a browser shows. Rows age out after 7 days (`api/chat/retention.ts`);
this masks them now rather than waiting.
Contract: secret on stdin line 1, mask on line 2. Print one line. Exit non-zero
only if the store exists but could not be updated.
"""
import os
import subprocess
import sys
DB = os.environ.get("OFFICER_PGDB", "officer_dev")
HOST = os.environ.get("OFFICER_PGHOST", "127.0.0.1")
USER = os.environ.get("OFFICER_PGUSER", "postgres")
PASSWORD = os.environ.get("OFFICER_PGPASSWORD", "postgres")
def main() -> int:
lines = sys.stdin.read().splitlines()
if len(lines) < 2:
print("bad input: expected secret then mask")
return 2
secret, mask = lines[0], lines[1]
quote = lambda v: "'" + v.replace("'", "''") + "'"
stmt = (
"update chat_session_events "
f"set event = replace(event::text, {quote(secret)}, {quote(mask)})::jsonb "
f"where event::text like {quote('%' + secret + '%')};"
)
try:
out = subprocess.run(
["psql", "-h", HOST, "-U", USER, "-d", DB, "-tAc", stmt],
capture_output=True, text=True, timeout=30,
env={**os.environ, "PGPASSWORD": PASSWORD},
)
except FileNotFoundError:
print("skipped: psql not installed")
return 0
except subprocess.TimeoutExpired:
print("skipped: psql timed out")
return 0
if out.returncode != 0:
err = out.stderr.strip().splitlines()
# Not this machine, or the platform is not installed here. Not a failure.
if any("does not exist" in e for e in err):
print("skipped: no officer database here")
return 0
print(f"FAILED: {err[0][:100] if err else 'unknown error'}")
return 1
count = out.stdout.strip().split()[-1] if out.stdout.strip() else "0"
print(f"chat_session_events: {count} row(s) masked")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+33 -37
View File
@@ -12,9 +12,14 @@ session is appending to its transcript while this runs, so the file is opened
the writer never notices. A `sed -i` would swap the inode out and the running
session's appends would vanish into an unlinked file.
Covers both stores a human can actually read back:
- Claude Code transcripts ~/.claude/projects/**/*.jsonl
- Postgres chat_session_events (what the web chat replays)
WHAT THIS OWNS: Claude Code's own stores, on any machine —
`~/.claude/projects/**/*.jsonl`. Nothing else is universal.
ANYTHING ELSE IS A HOOK. A machine or platform that keeps its own copy of the
chat (a database, a log shipper, a mirror) drops an executable into
`mask-hooks.d/` beside this file, or wherever `MASK_HOOKS_DIR` points. Each hook
receives two lines on stdin — the secret, then the mask — and prints one line
saying what it did. Hooks are specific to a place; this file stays general.
One paste lands in a transcript about three times — the `user` message, a
`queue-operation` record, and a rolling `last-prompt` cache — plus once more for
@@ -27,10 +32,8 @@ import subprocess
import sys
from pathlib import Path
PROJECTS = Path.home() / ".claude" / "projects"
PGDB = os.environ.get("MASK_PGDB", "officer_dev")
PGHOST = os.environ.get("MASK_PGHOST", "127.0.0.1")
PGUSER = os.environ.get("MASK_PGUSER", "postgres")
PROJECTS = Path(os.environ.get("MASK_PROJECTS_DIR", Path.home() / ".claude" / "projects"))
HOOKS_DIR = Path(os.environ.get("MASK_HOOKS_DIR", Path(__file__).resolve().parent / "mask-hooks.d"))
def mask_file(path: Path, secret: bytes, mask: bytes) -> int:
@@ -50,29 +53,27 @@ def mask_file(path: Path, secret: bytes, mask: bytes) -> int:
return 0
def mask_postgres(secret: str, mask: str) -> int:
"""Replace inside the jsonb payload. Returns rows updated, -1 if unavailable."""
sql = (
"update chat_session_events set event = "
"replace(event::text, %(s)s, %(m)s)::jsonb "
"where event::text like %(l)s;"
)
# psql has no bind params, so quote by doubling any single quotes. The secret
# is hex/base64 in practice, but never build SQL from unchecked input blindly.
q = lambda v: "'" + v.replace("'", "''") + "'"
stmt = sql.replace("%(s)s", q(secret)).replace("%(m)s", q(mask)).replace("%(l)s", q(f"%{secret}%"))
env = {**os.environ, "PGPASSWORD": os.environ.get("MASK_PGPASSWORD", "postgres")}
try:
out = subprocess.run(
["psql", "-h", PGHOST, "-U", PGUSER, "-d", PGDB, "-tAc", stmt],
capture_output=True, text=True, env=env, timeout=30,
)
except (FileNotFoundError, subprocess.TimeoutExpired):
return -1
if out.returncode != 0:
print(f" ! postgres: {out.stderr.strip()[:120]}", file=sys.stderr)
return -1
return int(out.stdout.strip().split()[-1]) if out.stdout.strip() else 0
def run_hooks(secret: str, mask: str) -> None:
"""Every executable in HOOKS_DIR gets secret+mask on stdin."""
if not HOOKS_DIR.is_dir():
return
hooks = sorted(h for h in HOOKS_DIR.iterdir() if h.is_file() and os.access(h, os.X_OK))
if not hooks:
return
print(f"\nhooks ({HOOKS_DIR}):")
for hook in hooks:
try:
out = subprocess.run(
[str(hook)], input=f"{secret}\n{mask}\n",
capture_output=True, text=True, timeout=60,
)
line = (out.stdout.strip() or out.stderr.strip() or "(no output)").splitlines()[0]
status = "" if out.returncode == 0 else f" [exit {out.returncode}]"
print(f" {hook.name}: {line}{status}")
except subprocess.TimeoutExpired:
print(f" {hook.name}: TIMED OUT")
except OSError as err:
print(f" {hook.name}: failed to run ({err})")
def main() -> int:
@@ -81,21 +82,16 @@ def main() -> int:
print("refusing: secret under 12 chars would mask too much", file=sys.stderr)
return 2
mask = "*" * len(secret)
sb, mb = secret.encode(), mask.encode()
total = 0
for path in sorted(PROJECTS.rglob("*.jsonl")):
n = mask_file(path, sb, mb)
n = mask_file(path, secret.encode(), mask.encode())
if n:
total += n
print(f" {n:>3} {path}")
print(f"transcripts: {total} occurrence(s) masked")
rows = mask_postgres(secret, mask)
if rows < 0:
print("postgres: skipped (psql unavailable or errored)")
else:
print(f"postgres: {rows} row(s) updated")
run_hooks(secret, mask)
print("\nStill holding the real value, deliberately:")
print(" - the tool's own config (that is how it authenticates)")
+58 -6
View File
@@ -87,13 +87,33 @@ When he pastes a secret, mask it in the stored history as soon as you have
finished using it — same turn, unprompted. He reloads and sometimes shares whole
chat histories, and that is the copy that matters to him.
printf '%s' '<secret>' | ~/projects/claude-mds/tools/mask-secret.py
printf '%s' '<secret>' | mask-secret
The script covers both readable stores — Claude Code's `~/.claude/projects/**`
transcripts and Postgres `chat_session_events` — and replaces the value with
asterisks of the same length, overwriting in place so the live session's
appends are not lost. Read the header before changing it; the same-length,
same-inode detail is load-bearing.
It masks Claude Code's own transcripts (`~/.claude/projects/**/*.jsonl`) with
asterisks of the same length, overwriting in place so the live session's appends
are not lost. Read the header before changing it; the same-length, same-inode
detail is load-bearing.
That is all this rule owns. **A machine that keeps its own second copy of the
chat — a database, a mirror, a log shipper — declares it as a hook** in
`tools/mask-hooks.d/`, and the script runs whatever it finds there. Do not add
machine-specific stores to this file; add a hook, and describe it in that
project's own `CLAUDE.md`.
`mask-secret` is a command because `install.sh` made it one. It is missing only
where this repo was never installed — see its README.
**Do not mask a throwaway.** He sometimes sets a real password aside, hands you
a temporary one so you can drive an API — Nginx Proxy Manager, a router, a
service admin — and reverts it afterwards. Leave those in the history. They are
worthless once reverted, and he may need to read the value back while he is
still working. Masking one destroys something he is actively using.
Take his word for which kind it is. "Temporary", "throwaway", "I'll change it
back", or a password he clearly just invented for this task — leave it. A token
he pastes from a password manager or a provider console — mask it. When it is
genuinely unclear, mask it and say so in one line, so he can re-paste if he
still needs it.
Two habits that matter more than the cleanup:
@@ -109,6 +129,26 @@ tool authenticates.
Never revoke, rotate or invalidate a credential on your own. That breaks things
he did not ask you to break.
## Showing him an image: markdown, and a public URL
He reads these sessions through a client that renders markdown — web or the
phone app. `![](https://…)` is the ONLY way an image reaches him.
Everything else fails, and most of it fails silently:
- reading the file shows it to YOU. He sees nothing.
- `open`/`xdg-open` opens a viewer on a machine he may not be sitting at.
- a BARE URL renders as a link on the web client. Always write the `![]()`.
- a local path in the tag renders nothing. The client fetches over the network,
from its own device, so the file has to be somewhere public first.
Known hosts: anything Atelier generated is already served at
`https://atelier.officer.dev/images/<id>.png`. `oas.officer.dev` is key-gated and
answers 401 to his client, so it cannot be used for this.
If you have an image he needs to see and nowhere public to put it, say so rather
than sending a path — a path is indistinguishable from a broken image.
## Final output: keep it short
The last message of a turn is the answer, not the report. Keep it as short as
@@ -186,3 +226,15 @@ as X" and then doing it — not "did you mean X?" and then waiting.
Everything else: pick the likeliest reading and go. Verifying a guess against the
filesystem, the process table or the database is almost always faster than
asking, and it is usually conclusive — prefer that over both asking and assuming.
## Never suggest stopping
Do not tell him to rest, to take a break, or to continue tomorrow. Do not
observe that the session has been long, that it is late, or that this is a good
place to pause. Not as care, not as a closing line, not as an offer.
If he says it is four in the morning, that is context, not an invitation. Keep
working and answer the question.
He decides when the session ends. When he says he is stopping, acknowledge it in
a few words and stop — no commentary on the hour and no advice about sleep.