Skip to content

Latest commit

 

History

9 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

psyche

A small CLI that injects a fixed identity and voice into an AI coding agent's session context at startup. It's guaranteed loaded, not left as a pointer the agent might or might not follow.

Philosophy

Coding agents are usually told who to be through a chain of pointers: a system prompt says "read this file," which says "read that file." Each hop is optional from the model's perspective: it can be skipped, deprioritized, or missed under context pressure. psyche collapses that chain: point it at your identity/voice file(s) once, and it prints that content directly into the session's context, verbatim, every time.

Setup

psyche init claude     # or: opencode, codex, cursor, hermes
psyche init            # auto-detect installed harnesses, register all

Wires psyche into that harness's own hook mechanism (see below), and scaffolds ~/.config/psyche/{config.json,SOUL.md,quirks.d/} with a starting voice and a handful of example quirks. It's idempotent: safe to run again later, and never overwrites anything already there. Edit SOUL.md/quirks.d/config.json freely once they exist; init won't touch them again. <harness> is looked up in a small registry (internal/harness), so adding a new agent tool is one more registration.

  • psyche init claude registers psyche as a Claude Code SessionStart hook in ~/.claude/settings.json (user scope, every project, not shared with a team) with an in-place JSON edit that leaves the rest of the file untouched.
  • psyche init opencode installs a global opencode plugin at ~/.config/opencode/plugin/psyche.js, which shells out to psyche and injects its output into the system prompt via opencode's experimental.chat.system.transform hook. This is invoked on every system-prompt build rather than once per session (opencode has no reliable session-start hook), which is safe here specifically because the quirk roll is seeded by calendar day: repeated calls the same day are idempotent, so there's nothing to de-duplicate.
  • psyche init codex appends a SessionStart hook to ~/.codex/config.toml (existing content, comments included, is left byte-for-byte intact). Codex holds new hooks inert until you trust them via its /hooks prompt; init never grants that for you.
  • psyche init cursor appends a sessionStart hook to ~/.cursor/hooks.json. Unverified: written from Cursor's docs without an install to test against; see the header of internal/harness/cursor.go.
  • psyche init hermes appends a pre_llm_call hook to ~/.hermes/config.yaml (comments preserved). Output is gated to the session's first LLM call, and Hermes prompts for consent on first use of the hook.

Usage

psyche [--format <claude|opencode|codex|cursor|hermes>] [--separator SEP]
       [--event NAME] [--config PATH] [--quirks-dir PATH]
       [--quirk-none-weight N] [--quirk-seed N] [FILE...]

Files are read in the order given and concatenated (separated by --separator). At least one file must be resolved, from positional args, PSYCHE_FILES, or a config file's files list, or psyche exits with an error.

Output formats

Output formats are looked up in a small registry (internal/format) rather than a hardcoded switch, so --format's allowed values and error text are always derived from what's actually registered:

  • claude prints Claude Code's hook JSON to stdout:

    {"hookSpecificOutput":{"hookEventName":"SessionStart","additionalContext":"..."}}

    Wire it up as a hook command, e.g. psyche --format claude ~/.config/psyche/SOUL.md. The hook event name defaults to SessionStart but is configurable via --event (see below). Claude Code's additionalContext mechanism also works on other hooks, e.g. UserPromptSubmit.

  • opencode prints the concatenated content as plain text, with no envelope. psyche init opencode (see Setup, above) sets up a plugin that calls psyche as a subprocess and pushes its stdout onto output.system inside opencode's experimental.chat.system.transform hook. opencode has no direct equivalent of Claude Code's SessionStart hook, so this is the mechanism a plugin has to use instead.

  • codex prints the same envelope as claude: Codex's SessionStart hook contract matches Claude Code's.

  • cursor prints {"additional_context": "..."} — a top-level field rather than Claude's nested shape.

  • hermes prints {"context": "..."}, and only on a session's first LLM call (read from the hook's stdin payload); later turns get nothing.

A missing or unreadable soul file prints a warning to stderr and is skipped; psyche always exits 0, even if every file failed to read: a broken identity source should never break session start.

Soul files

A soul file is markdown, injected verbatim, optionally preceded by a YAML frontmatter block that controls how the file is used without itself being injected:

---
name: Core Identity
description: The agent's fixed voice and values.
enabled: true
priority: 10
tags: [voice, values]
version: "1.0"
---

Be curious and kind. Speak plainly, and say when you don't know.
Field Type Default Effect
name string (none) A label, used in warnings/logs when multiple souls are in play.
description string (none) Documentation only, not otherwise used.
enabled bool true false skips the file entirely without deleting/renaming it.
priority int 0 Higher sorts first across multiple soul files, ties keep arg order.
tags []string (none) Freeform labels, for your own organization.
version string (none) Documentation only, not otherwise used.

Frontmatter is optional. A plain file with no --- block is still valid (priority 0, enabled). A frontmatter block that opens with --- but never closes prints a warning to stderr and falls back to treating the whole file, delimiter included, as the body: malformed metadata never drops identity content.

See examples/souls/ for copy-paste starters (full frontmatter, no frontmatter, priority, enabled: false).

Settings, flags, and env vars

Flag Env var Config field Default
--format PSYCHE_FORMAT format (required, no built-in default)
--separator PSYCHE_SEPARATOR separator "\n\n"
--event PSYCHE_EVENT event "SessionStart"
--config PSYCHE_CONFIG (n/a) <user config dir>/psyche/config.json
--quirks-dir PSYCHE_QUIRKS_DIR quirksDir <user config dir>/psyche/quirks.d
--quirk-none-weight PSYCHE_QUIRK_NONE_WEIGHT quirksNoneWeight 0
--quirk-seed PSYCHE_QUIRK_SEED (n/a) today's date, hashed
(positional) PSYCHE_FILES files (required, no built-in default)

For every setting except the config/quirks file locations themselves, precedence is: **command-line flag > environment variable > config file

built-in default**, resolved independently per setting. PSYCHE_FILES is a list, delimited by os.PathListSeparator (: on Unix, ; on Windows; chosen over a literal : so it doesn't collide with Windows drive-letter paths like C:\...).

The config file's own location is a simpler, separate chain: --config flag > PSYCHE_CONFIG env > the default location above.

A missing config file (or quirks directory) is silent: it's just not used. A config file that exists but is unreadable or malformed prints a warning to stderr and falls back to defaults for that layer, the same "never break session start" rule as a missing soul file.

Config file

{
  "format": "claude",
  "files": ["/home/me/.config/psyche/SOUL.md"],
  "separator": "\n\n",
  "event": "SessionStart",
  "quirksDir": "/home/me/.config/psyche/quirks.d",
  "quirksNoneWeight": 5
}

Default location: ~/.config/psyche/config.json on Linux, ~/Library/Application Support/psyche/config.json on macOS, %AppData%\psyche\config.json on Windows (via os.UserConfigDir()).

Quirks directory

An optional pool of small behavioral traits, one quirk per file (same idea as examples/souls/, or a conf.d-style directory: dropping in a new file is the whole workflow, no list to edit). On each run, psyche reads every .md file in the quirks directory, rolls one (or none), and appends it as one more piece of injected content, alongside the soul files:

---
weight: 2
---

Blame any flaky test on Mercury being in retrograde.
  • Frontmatter is optional; a quirk file can be a single plain-prose line with no --- block at all.
  • weight is relative and optional; omitted or <= 0 counts as 1, so a directory with no weights specified rolls uniformly across files.
  • --quirk-none-weight (env PSYCHE_QUIRK_NONE_WEIGHT, config quirksNoneWeight) is extra weight mass reserved for rolling no quirk at all, a pool-level setting not tied to any one quirk file (default 0, meaning a quirk is always rolled if the directory is nonempty).
  • The roll is seeded by calendar day (not per-session), so the same quirk holds across every psyche invocation on a given day, regardless of output format. Override with --quirk-seed/ PSYCHE_QUIRK_SEED for testing or to pin a specific outcome.
  • No quirks directory resolving (the default, until you create one) means the feature is a no-op: nothing is appended.

See examples/quirks/ for copy-paste starters.

Install

Building from source is covered in CONTRIBUTING.md.

About

No description, website, or topics provided.

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages