Skip to content

feat(personas): named system-prompt overrides for hands run#27

Merged
askalf merged 1 commit into
mainfrom
feat/personas
Apr 29, 2026
Merged

feat(personas): named system-prompt overrides for hands run#27
askalf merged 1 commit into
mainfrom
feat/personas

Conversation

@askalf
Copy link
Copy Markdown
Owner

@askalf askalf commented Apr 29, 2026

Summary

Adds two new flags to `hands run`:

```
--persona use a named persona (bundled or ~/.hands/personas/.md)
--system-prompt use an arbitrary prompt file (bypasses --persona)
```

The resolved prompt replaces hands' default OS-aware system prompt in SDK mode. Default behavior (no flag) is unchanged.

Bundled personas:

Name Behavior
`minimal` Short, no constraints, "use tools when useful"
`thorough` Take initiative, exhaustive code with comments
`concise` Terse, one-or-two sentences, no preamble
`security-aware` Confirm-before-destructive; read-only OK without

Resolution order for `--persona`:

  1. `~/.hands/personas/.md` (user override; takes precedence)
  2. One of the four bundled personas above
  3. Error: `persona '' not found` with the bundled set listed

`--system-prompt ` bypasses persona lookup entirely — direct file load.

Why this is safe to ship

dario research #172 confirmed that Anthropic's billing classifier doesn't fingerprint system prompt content — content, length, and block count are not classifier inputs as long as the rest of the request shape (effort, max_tokens, tool array, body field order, billing tag, anthropic-beta) is preserved. Combined with hands routing through dario for OAuth subscription billing, that means swapping the system prompt does NOT flip billing from `five_hour` to `overage`. Personas are the operator-facing surface for that capability.

Mutex + early-exit

`--persona` and `--system-prompt` are mutually exclusive — both set exits 1 with a clear error before doing anything else. Resolution happens BEFORE config load and screenshot capture, so "persona not found" surfaces fast with a helpful error message that lists the bundled set.

Scope: SDK mode only (for now)

Plumbed through `runSdkMode` (the API-key / dario-routed path). CLI mode (spawning `claude --append-system-prompt`) doesn't plumb `--persona` through yet — the integration there is meaningfully different (`--append` vs replace) and deserves its own PR. Today's verification is SDK-mode + dario.

Test plan

  • Unit: `test/personas.test.mjs` — 7 cases covering the bundled set, unknown name (helpful error message), user-file override beats bundled, content trimming on user-file path, explicit `--system-prompt` path loading, missing file throws, `listBundledNames` identity. HOME-redirect via tmpdir for user-file tests.
  • Local: `npm test` passes 56/56.
  • CI: standard hands checks.

Notes

  • New file: `src/personas.ts`. Three modified: `run.ts` (resolution + plumbing), `sdk-mode.ts` (accepts `systemPromptOverride`), `cli.ts` (flags). Pure additive on every existing path; default behavior unchanged.
  • This is the second of three features pending today (auto-detect dario was #26). Replay mode is the third — separate PR.

Adds two new flags to `hands run`:

  --persona <name>      use a named persona (bundled or user file)
  --system-prompt <path> use an arbitrary prompt file (bypasses --persona)

Resolution order for --persona:
  1. ~/.hands/personas/<name>.md  (user override, takes precedence)
  2. one of the four bundled personas (minimal, thorough, concise,
     security-aware)
  3. error: persona '<name>' not found, with the bundled set listed

The resolved prompt REPLACES hands' default OS-aware system prompt
in SDK mode. Default behavior (no flag) is unchanged. CLI mode
(spawning `claude`) doesn't plumb --persona through yet — adding
that is a future PR; the `--append-system-prompt` integration there
is meaningfully different from a string replacement.

Why this is safe to ship: dario research (askalf/dario#172) confirmed
that Anthropic's billing classifier doesn't fingerprint system prompt
content — content, length, and block count are not classifier inputs
as long as the rest of the request shape (effort, max_tokens,
tool array, body field order, billing tag, anthropic-beta) is
preserved. Combined with hands routing through dario for OAuth
subscription billing, that means swapping the system prompt does
NOT flip billing from five_hour to overage. Personas are the
operator-facing surface for that capability.

Bundled personas, by intent:

  minimal        short, no constraints, "use tools when useful"
  thorough       take initiative, exhaustive code with comments
  concise        terse, one-or-two sentences, no preamble
  security-aware confirm-before-destructive, read-only OK without

Mutex: --persona and --system-prompt are mutually exclusive — both
set is a clear operator error and we exit 1 before burning any
work. Resolution happens BEFORE config load and screenshot
capture so "persona not found" surfaces with a clear message
fast.

Test coverage: test/personas.test.mjs — 7 cases covering the
bundled set, unknown name (with helpful error message listing
the bundled set), user-file override taking precedence over
bundled, content trimming on the user-file path, explicit path
loading via --system-prompt, missing file throwing Error,
listBundledNames identity. HOME-redirect via tmpdir for the
user-file tests (mocks ~/.hands/ without touching the user's
real config).
@askalf askalf merged commit 171da8a into main Apr 29, 2026
5 checks passed
@askalf askalf deleted the feat/personas branch April 29, 2026 23:41
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