# dsh-where-am-i > Neofetch for DeepSeek Harness. **English** | [简体中文](./README.zh-CN.md) A DeepSeek Harness (dsh) plugin that injects a **neofetch-style device info block** into **every brand-new session** — OS distro, kernel, architecture, hostname, CPU, memory, uptime, timezone, locale, shell/terminal, and Node version. It is injected **exactly once, when a new session takes its first step**, and then persists in that session's own history; existing sessions are completely untouched. ## Behavior at a glance | Scenario | Behavior | |---|---| | **Brand-new session** (first message after creation) | a device-info user message is injected; the model sees it at the start of the conversation and it persists until compaction | | **Existing sessions** (created before a restart, resumed, or continued) | the system prompt is **byte-identical** and **no** message is appended — the request prefix and its KV cache are fully preserved | | **Later steps of the same session** | never re-injected (the message is already durable history) | | **Subagent sessions** (ephemeral workers spawned by the subagent/workflow tools) | skipped by default (avoids repeating per fan-out); opt in with `includeSubagents: true` | ## Usage Install into a dsh profile (once published, by package name): ```bash dsh plugin --profile web add "@gausszhou/dsh-where-am-i" ``` For local development use a link dependency (same pattern as `dsh-opencode-session-id`): add `"@gausszhou/dsh-where-am-i": "link:/home/gauss/Code/gausszhou/dsh-where-am-i"` to `~/.dsh/profiles/web/package.json` dependencies and list it in `dsh.profile.bundles`; the plugin's own `cordis.patch.yml` (via `dsh.bundle.patch`) performs the insert. After installing, **restart dsh web** (`systemctl --user restart dsh-web`) so the bundle takes effect. Zero configuration needed. ## What the model sees in a new session (real machine) ```markdown System info (the machine this dsh session runs on; sampled at session start): host: gauss-KP user: gauss os: Ubuntu 26.04 LTS kernel: 7.0.0-29-generic arch: x86_64 cpu: 6 × Intel(R) Core(TM) i5-8400 CPU @ 2.80GHz memory: 7.9 GiB used / 15.0 GiB total uptime: 1d 4h 3m timezone: Asia/Shanghai (GMT+08:00) locale: zh_CN.UTF-8 shell: /bin/bash term: dumb node: v24.19.0 ``` ## Configuration All options are optional, with the defaults shown: | Key | Default | Meaning | |---|---|---| | `fields` | all 13 | emit only the listed field lines, in the default order, e.g. `[os, arch, timezone]`; unknown names are dropped; an empty-after-filter list falls back to all | | `includeSubagents` | `false` | when `true`, also inject into subagent sessions (skipped by default to avoid repeating per fan-out) | | `verbose` | `false` | print the injected snapshot to stdout (visible via `journalctl -u dsh-web` on a web deployment) | Field list (emission order): `host` `user` `os` `kernel` `arch` `cpu` `memory` `uptime` `timezone` `locale` `shell` `term` `node`. Fields that cannot be sampled (e.g. `shell` on Windows without a `SHELL` variable) are omitted automatically. ```yaml # ~/.dsh/profiles/web/cordis.patch.yml - id: where-am-i config: fields: [os, kernel, arch, cpu, memory, timezone] ``` ## Design: what dsh prompts already carry Before implementing, the existing prompt surface was surveyed (`dsh-system-prompt` section/variable registration, `dsh-agent-loop` variable providers, the runtime-context packages). Existing facts are listed below; this plugin **only fills the gaps and never duplicates**: | Existing fact | Source | Location | |---|---|---| | `You are an AI agent powered by DeepSeek Harness.` | `dsh-system-prompt` | system prompt section `harness:identity`, order −100 | | DSH source checkout path | `dsh-app-boot` / `dsh-web-app` | section `harness:source`, order −99 | | Web GUI URL | `dsh-web-app` (web profile only) | section `app:web-surface`, order −98 | | Deployment persona | config | section `deployment:persona`, order 0 | | Tool guidance (read/write/edit/bash/web_search…) | each `dsh-tool-*` | sections `tool:*`, orders 100–199 | | `{{provider}}` `{{model}}` `{{cwd}}` (session cwd, evaluated per assembly) | `dsh-agent-loop` | prompt variables | | File sandbox policy, approval policy | `dsh-sandbox-policy` / `dsh-user-approval` | runtime context (user-role snapshots) | | Timestamp + **browser** timezone + elapsed duration | `dsh-time-context` (not enabled in the default composition) | runtime context | | tmux session/window/pane location | `dsh-tmux-context` (not in the web composition) | runtime context | | `AGENTS.md` / `CLAUDE.md` workspace instructions | `dsh-agent-instructions` | durable user message | **Completely missing** (what this plugin adds): OS/distro name, kernel release, CPU architecture, hostname, CPU model and core count, total/free memory, uptime, host locale, shell/terminal, Node version, and the host process timezone. ### Deliberate omissions - **`cwd`** — already provided live by the `{{cwd}}` prompt variable, and the harness explicitly warns against inferring it from the checkout path. - **date / current time** — a frozen wall clock in durable history would go stale; live timestamps come from `dsh-time-context` per request when enabled. - **ASCII logo / desktop wallpaper** — token noise with no agent value. ## Why a persistent user message, not a system prompt section The first version contributed a `systemPrompt.section()`, but that renders on *every step of every session* once mounted — it can never be "once", and it rewrites the shared system-prompt prefix of already-running sessions (KV cache invalidated from the first changed token). The current implementation uses the same seam as `dsh-time-context` / `dsh-tmux-context`: a **prepended `agent/pre-step` waterfall listener** that first delegates downstream (`await next()`) and, only when the step enters, appends **one durable, source-attributed user message** to the batch. The agent loop persists every message in the batch (`session.append("user/message", …)`) after `step/start`, so: - the injected message becomes **that session's own history** (visible from the start, retained until compaction); - **no system prompt section is registered** → existing sessions' system prompt is byte-identical to before, prefix and KV cache fully preserved; - later steps — of this session or any other — see the message already in history and never inject again (across restarts and plugin hot reloads, because the decision reads only the session log itself). "Brand-new session" is decided from the session log alone: at pre-step time, `agent.session.events` holds no `user/message`, `assistant/message`, or `tool/result` event yet (the in-flight prompt of step 1 is persisted only after the step enters — `dsh-time-context`'s README states this explicitly). Any resumed, continued, or already-injected session has at least one such event and is skipped, forever. A batch with no user-sourced message at all (plugin-only runtime-context or scheduled turns) is also never injected into. Device facts are sampled **at injection time** (the new session's first step), so uptime/memory are as fresh as possible; the message is per-session history, so byte-level differences between sessions cost nothing. ### Bottom line for "existing session prefixes" - System prompt: the plugin registers **no section**; `renderPrompt` output for existing sessions is identical to before installation (asserted by the integration test). - Session history: no message is appended to existing sessions. - The only affected surface is **new sessions**: injection happens before their first request, i.e. it is part of that session's own prefix — there is no old cache to invalidate. ## Implementation details - **Zero subprocesses**: no `sw_vers`, no `uname` — everything comes from `node:os`, one synchronous `/etc/os-release` read (Linux/WSL: `PRETTY_NAME`, falling back to `ID VERSION_ID`), and `Intl`. - Architecture names follow neofetch's mapping: `x64→x86_64`, `arm64→aarch64`, `ia32→i386`, `arm→armv7l`. - macOS: the marketing version is derived from the Darwin release (Darwin 24 ↔ macOS 15, 19 ↔ 10.15; older releases are not guessed). - Timezone is the **host process** timezone (IANA name + current UTC offset, DST-aware) — a different concept from `dsh-time-context`'s browser zone. - The injected message satisfies the harness message contract (`assertMessageEventShape`: non-empty `id`, `role: "user"`, `source.kind`, `content` array); its source mirrors time-context's plugin-snapshot shape (`{ kind: 'plugin', plugin: 'where-am-i', form: 'snapshot', sections: [...] }`) so UI/trajectory attribution is consistent. - `inject: ["agents"]` (same as time-context): the plugin does not load in a tree without the agents registry. ## Verification ```bash node test/verify.mjs # unit: os-release parsing / arch mapping / macOS naming / byte & uptime formatting / timezone offset / config normalization / render subset / decision gates (user source, history, subagent) / message shape / no-cwd-no-date invariants + real-machine snapshot node test/smoke-apply.mjs # wiring: new session injects once, existing session zero injection, same session never re-injects, no-user-source batches skipped, reject/abort passthrough, subagent gate, fields subset node test/integration-real.mjs # real cordis + dsh-system-prompt + dsh-session: system prompt untouched, injected message persisted by a real Session and visible in derived history, no re-injection ``` ## Notes & limitations - The snapshot is sampled at each new session's **first step**: uptime/memory go stale during that session (neofetch-at-session-start semantics; the message is history and is not refreshed). - Without `/etc/os-release` on non-Windows/macOS systems, the `os` line falls back to `os.version()`/`os.type()` and may be less friendly than a distro name. - The `user` line is not redacted — the model can see the local username running dsh (like neofetch). Drop `user`/`host` via `fields` if you care about privacy. - Host-machine facts are reported; if dsh runs inside a container or remote execution, the kernel reported is the container's/remote one (e.g. WSL shows the WSL kernel). - The message costs tokens on every request until compaction hides it; this is why existing sessions and subagents are excluded by default — the cost is paid once, only by genuinely new conversations.