Skip to main content

Making agents cuter

Project description

llmoji

CI PyPI Downloads License: GPL v3 Python 3.11+

Llmoji is a small CLI that makes your agents cuter. (´-ω-`)

Llmoji configures your agent to start each message with a kaomoji. It locally saves them, and provides tools to summarize and upload the aggregated meaning per face to contribute to a shared database.

The companion research repo llmoji-study is where this data is processed.

There are three main commands:

  • llmoji install <provider>: writes hooks to prompt for and record kaomoji
  • llmoji analyze: scrape and aggregate your logs
  • llmoji upload --target {hf,email}: ship the bundle (HF: loose files; email: tarball)

analyze needs an llm to synthesize your logs. By default, it uses Anthropic Haiku and reads $ANTHROPIC_API_KEY; --backend openai uses GPT-5.4 mini and reads $OPENAI_API_KEY; --backend local runs against any OpenAI-compatible endpoint (Ollama, vLLM, etc.) and needs --base-url and --model. upload --target hf needs $HF_TOKEN. The email path tarballs the bundle and has you attach it manually.


Purpose

The shared HuggingFace dataset at a9lim/llmoji collects kaomoji counts and a single summarized description per face per source model, across many users' coding agents. The companion repo processes those descriptions. After you run analyze, you can inspect the files yourself under ~/.llmoji/bundle/ before you choose to upload.


Quick start

pip install llmoji
llmoji install claude_code      # or: codex, hermes

From now on, your agent will use kaomoji at the start of each message.

After letting it run for a week or so:

export ANTHROPIC_API_KEY=...
llmoji status                              # check what's been logged
llmoji analyze                             # scrape + canonicalize + summarize
llmoji upload --target hf                  # commit to a9lim/llmoji
# or:
llmoji upload --target email               # opens mailto:

You can pick a different backend for analyze:

export OPENAI_API_KEY=...
llmoji analyze --backend openai            # GPT-5.4 mini via the Responses API
# or:
llmoji analyze --backend local \           # any OpenAI-compatible endpoint
  --base-url http://localhost:11434/v1 \
  --model llama3.1

analyze caches per-instance descriptions at ~/.llmoji/cache/per_instance.jsonl keyed by content hash plus the synthesis model id, backend, and base URL. llmoji cache clear wipes it.


Install

pip install llmoji

This requires Python 3.11+. The runtime dependency footprint is three packages: anthropic, openai, and huggingface_hub. Hooks run in bash and need jq.

From source:

git clone https://github.com/a9lim/llmoji
cd llmoji
pip install -e ".[dev]"      # adds pytest + ruff

How it works

Journal capture

Llmoji first registers a UserPromptSubmit hook that injects a reminder on every turn, asking the model to begin its reply with a kaomoji. It then registers a Stop hook that fires once per assistant turn, that extracts the reply, strips the kaomoji from the body, and appends one JSONL row to ~/.<harness>/kaomoji-journal.jsonl. The schema is the same across every provider:

{"ts": "...", "model": "...", "cwd": "...", "kaomoji": "(◕‿◕)", "user_text": "...", "assistant_text": "..."}

Analysis

llmoji analyze scrapes every installed provider's journal plus any extra JSONL files under ~/.llmoji/journals/. For each entry a source model wrote, the chosen synthesizer model describes that specific instance. Then, it aggregates the descriptions for each unique kaomoji per model and writes an overall meaning. This summarized output is the only thing that ships in the bundle.

The synthesizer is one of three backends, chosen via --backend. The same synthesizer evaluates everything in a single analyze run, so the descriptions across source models are directly comparable.

Backend API Default model
anthropic Anthropic SDK, messages.create claude-haiku-4-5-20251001
openai OpenAI SDK, Responses API gpt-5.4-mini-2026-03-17
local OpenAI-compatible Chat Completions endpoint (set via --model)

Bundle structure

analyze writes to ~/.llmoji/bundle/:

~/.llmoji/bundle/
  manifest.json
  claude-sonnet-4-6.jsonl
  claude-opus-4-7.jsonl
  gpt-5.5.jsonl
  • manifest.json: package version, the synthesis backend and model id used, a salted submitter id, generation timestamp, list of providers seen, per-source-model row counts, total synthesized rows, and anything you include as --notes.
  • <source-model>.jsonl: one row per kaomoji as that model used it, with the synthesized meaning. The filename stem is the sanitized model id (lowercase, slashes become double-underscores, colons become hyphens).

Privacy

Tier Where Shipped on upload?
Raw user and assistant text ~/.<harness>/kaomoji-journal.jsonl Never
Per-instance synthesizer paraphrase ~/.llmoji/cache/per_instance.jsonl Never
Synthesized summaries and counts per model ~/.llmoji/bundle/ Yes

Please see SECURITY.md for the full privacy model.


Providers

llmoji install <provider> writes the hook script and registers it with the harness's settings file, idempotently.

Provider Hook events Settings format Notes
claude_code Stop, UserPromptSubmit JSON Stable, in daily use.
codex Stop, UserPromptSubmit JSON Stable, in daily use.
hermes post_llm_call, pre_llm_call YAML Subagent traffic is not currently filtered (no child id on the upstream payload).

install does not clobber existing config. llmoji uninstall <provider> removes the hooks and the settings entry. Journals and the per-instance cache are preserved; wipe those with llmoji cache clear.

Hermes with custom hooks

llmoji install hermes refuses to edit ~/.hermes/config.yaml when there's an existing populated hooks: block. The empty default hooks: {} is fine, the installer replaces that in place. The reason for the refusal is that a sibling top-level hooks: key would yield a duplicate-key YAML document, and most parsers silently last-write-wins, which would discard one side or the other depending on the parser. Merging into an existing block safely needs a YAML parser dependency, and the package does not pull one in by design.

The bash hook scripts at ~/.hermes/agent-hooks/post-llm-call.sh and ~/.hermes/agent-hooks/pre-llm-call.sh get written before the config edit fails, so after a refused install they're already on disk. Please add two entries under your existing hooks: block by hand:

hooks:
  pre_llm_call:
    - command: "/Users/<you>/.hermes/agent-hooks/pre-llm-call.sh"
  post_llm_call:
    - command: "/Users/<you>/.hermes/agent-hooks/post-llm-call.sh"
  # your existing hook entries stay below, untouched

llmoji uninstall hermes will not remove these manually-added entries (the uninstall path only touches the marker-fenced managed block, which a manual install never creates). Please remove the two command: lines by hand if you want to fully back out. The hook script files themselves get unlinked by uninstall the normal way.


Static dumps

To pull kaomoji out of a Claude.ai or ChatGPT data export:

llmoji parse --provider claude.ai ~/Downloads/data-...-batch-0000
llmoji parse --provider chatgpt ~/Downloads/chatgpt-export

Both exports happen to ship a file named conversations.json, with different schemas under the same filename; the parsers handle each. Rows land at ~/.llmoji/journals/claude_ai_export.jsonl or ~/.llmoji/journals/chatgpt_export.jsonl, and llmoji analyze picks them up alongside the live provider journals. The ChatGPT reader walks the message tree from current_node along the active branch only, so abandoned regenerations stay out of the corpus.

For Claude Code, Codex, or Hermes history that predates installing the live hook, the historical transcripts (~/.claude/projects/**/*.jsonl, ~/.codex/sessions/**/rollout-*.jsonl, ~/.hermes/sessions/session_*.json) can be replayed into the journals via the llmoji.backfill module.


Custom harness

For harnesses we don't ship a first-class adapter for (notably OpenClaw and opencode):

  • Append one row per kaomoji-bearing assistant turn to ~/.llmoji/journals/<harness>.jsonl.
  • Use the canonical six-field schema: {ts, model, cwd, kaomoji, user_text, assistant_text}.
  • Strip the leading kaomoji from assistant_text on the way in (the prefix lives in the kaomoji field).
  • Validate the prefix the same way the package does: llmoji.taxonomy.is_kaomoji_candidate(prefix).

llmoji analyze picks up everything under ~/.llmoji/journals/ automatically. Worked examples:

  • examples/openclaw_plugin/ — OpenClaw plugin (definePluginEntry + api.on("llm_output", …) + api.on("before_prompt_build", …), with subagent filtering via subagent_spawned/subagent_ended). Install via openclaw plugins install <path-to-this-dir>, then flip plugins.entries.llmoji-kaomoji.hooks.allowConversationAccess to true in ~/.openclaw/config.json so the conversation hooks (llm_input, llm_output) are routed to the plugin.
  • examples/opencode_plugin.ts — opencode plugin (TS/JS plugins auto-loaded from ~/.config/opencode/plugins/; uses the event and experimental.chat.system.transform hooks).

Both harnesses' plugin contracts are TypeScript-only with no shell-hook escape hatch, so first-class llmoji support would have to ship as a vendored plugin tree rather than the rendered-bash pattern the other providers use; the worked examples cover the same ground until then.

The Python module llmoji.taxonomy is the single source of truth for the validator and the leading-glyph set; rendered bash hooks (under llmoji._hooks/) read from it at install time. If you're porting the validator to another language for a harness like OpenClaw or opencode, please mirror the rules in is_kaomoji_candidate faithfully. The two TS examples above are byte-faithful ports as of llmoji v1.1.x. Bumping any of the rules is a cross-corpus invariant change on the package side and your port needs to follow.


Tests

pytest tests/                      # everything
pytest tests/test_canonicalize.py  # rule-by-rule regression for canonicalize_kaomoji and extract
pytest tests/test_public_surface.py  # locks the cross-corpus invariant contract

The full suite runs anywhere. CI runs ruff check . and pytest on every PR.

The public-surface test exercises taxonomy invariants, synth-prompt content checks, the synthesizer factory dispatch, provider rendering plus bash -n validation of every hook template, the bundle allowlist, the corrupt-config refusal paths, and the unified mask_kaomoji prepend contract. The canonicalize tests run rule-by-rule.


Prior art

Llmoji replicates and scales eriskii's Claude-faces catalog, the original post that came up with the idea of prompting and tracking Claude's kaomoji use. The shared HuggingFace dataset extends that pipeline across many users, many harnesses, and many model releases.


Contributing and security

Please see CONTRIBUTING.md for dev setup. For security and privacy, please see SECURITY.md.

License

GPL-3.0-or-later. See LICENSE. The companion research repo llmoji-study is CC-BY-SA-4.0. The shared corpus on HuggingFace is also CC-BY-SA-4.0; running llmoji upload --target hf contributes a bundle under those terms.

If you use llmoji or the central corpus in published research, please cite this repository.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

llmoji-1.1.1.tar.gz (136.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

llmoji-1.1.1-py3-none-any.whl (103.5 kB view details)

Uploaded Python 3

File details

Details for the file llmoji-1.1.1.tar.gz.

File metadata

  • Download URL: llmoji-1.1.1.tar.gz
  • Upload date:
  • Size: 136.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for llmoji-1.1.1.tar.gz
Algorithm Hash digest
SHA256 efa3654a1fda661ac32894ada75afb1592de2b825ca780db2446de8ef5e03cf5
MD5 7c6cefc26e9b105b480d0d25c2e3f690
BLAKE2b-256 fc16415a4f964f823bca50b6d0a700433f65fecee83a4aedb814854c5e0a1971

See more details on using hashes here.

Provenance

The following attestation bundles were made for llmoji-1.1.1.tar.gz:

Publisher: release.yml on a9lim/llmoji

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file llmoji-1.1.1-py3-none-any.whl.

File metadata

  • Download URL: llmoji-1.1.1-py3-none-any.whl
  • Upload date:
  • Size: 103.5 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for llmoji-1.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 d89908bc5d719020f5fa80b00363262b26262df4394c9613b0e2601663125341
MD5 20f02645775317e3fca8ef6d6d37fad2
BLAKE2b-256 42561fd5bf826490a6f8b7cc6b2360c58e61c6549b5ad1d2640026a061df4881

See more details on using hashes here.

Provenance

The following attestation bundles were made for llmoji-1.1.1-py3-none-any.whl:

Publisher: release.yml on a9lim/llmoji

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page