Quick start in 5 minutes

Build a minimal cron plugin that checks in every hour and queues a note. No external dependencies, no core imports.

1
Create the plugin directory
All plugins live under $FUROSHIKI_DATA/plugins/ (default ~/.furoshiki/plugins/).
2
Write PLUGIN.md
YAML frontmatter declares name, hooks, and permissions. The markdown body is for humans.
3
Write the hook script
Import furoshiki_plugin_sdk (always on PYTHONPATH). Use the PluginClient to read data or queue notes.
4
Reload & verify
furoshiki plugins reload merges your cron hook into schedules.json. furoshiki plugins list confirms it's active.
bash terminal
# 1. Create directories
mkdir -p ~/.furoshiki/plugins/my-plugin/scripts

# 4. Reload and verify
furoshiki plugins reload
furoshiki plugins list
yaml ~/.furoshiki/plugins/my-plugin/PLUGIN.md
---
name: my-plugin
version: 1.0.0
description: "Hourly check-in"
author: user
active: true
hooks:
  - type: cron
    schedule: "0 * * * *"   # every hour, UTC
    entry: scripts/run.py
    timeout: 30
permissions:
  output_mind_queue: true
---

# My Plugin
Runs every hour and queues a note.
python ~/.furoshiki/plugins/my-plugin/scripts/run.py
from furoshiki_plugin_sdk import PluginClient

client = PluginClient()  # reads env vars automatically

client.enqueue(
    kind="note",
    summary="Hourly check ran OK",
    dedupe_key="my-plugin:hourly",
)
client.log("info", "my-plugin finished")
Tip: The dedupe_key field prevents duplicate entries — if a row with the same key already exists in mind_queue, the insert is silently skipped.

Choosing the right hook

Each hook fires at a specific lifecycle event. Pick the one that matches when you want to act.

cron
Run on a schedule (every N minutes, daily, etc.)
stdout: optional mind_queue_item NDJSON
pre_conversation
Add context to every conversation start
stdout: {"label","content","priority"}
post_conversation
React after a session ends (logging, follow-ups)
stdout: ignored (use API for side effects)
pre_reflection
Enrich Furoshiki's reflection prompt
stdout: {"label","content"}
post_reflection
Act on completed reflection output
stdout: ignored (receives full reflection JSON)
callable
Give the LLM a tool it can call on demand
stdout: any JSON object (returned to model)
post_inbound_signals
Tweak conversation-mode classification
stdout: {"conversation_mode","prompt_addendum"}

Hook input / output contracts

Arguments are passed by plugin_loader. Your script parses them with argparse and writes the required JSON to stdout.

cron

Runs standalone via run_plugin_scheduled.py. No required arguments, no required stdout. Optionally emit mind_queue_item NDJSON lines:

python
import json
import sys

item = {
    "type": "mind_queue_item",
    "kind": "note",
    "summary": "Cron job finished",
    "dedupe_key": "cron:status:2026-04-10",
}
print(json.dumps(item))  # loader mediates this into mind_queue

pre_conversation

Args: --mode pre_conversation --state-json <path>

python
import json, argparse
p = argparse.ArgumentParser()
p.add_argument('--mode')
p.add_argument('--state-json', dest='state_json')
args = p.parse_args()

print(json.dumps({
    "label": "my_context",
    "content": "Background task still running (2h 15m)",
    "priority": 50,
}))

callable

Args: --mode callable --args-json <path> --state-json <path>. Return any JSON the model should receive.

python
import json, argparse

p = argparse.ArgumentParser()
p.add_argument('--mode')
p.add_argument('--args-json', dest='args_json')
p.add_argument('--state-json', dest='state_json')
args = p.parse_args()

with open(args.args_json) as f:
    call_args = json.load(f)

# ... do work ...

print(json.dumps({"running": True, "uptime_sec": 8100}))
Important: Hooks that require stdout JSON (pre_conversation, pre_reflection, callable) must emit exactly one JSON object as the last output line. Invalid JSON causes the hook to be skipped with a bad_output log entry.

Using furoshiki_plugin_sdk

The SDK is always on PYTHONPATH for plugin subprocesses — no pip install required. It is stdlib-only and wraps all Plugin API endpoints.

python
from furoshiki_plugin_sdk import PluginClient, PluginAPIError

client = PluginClient()  # reads FUROSHIKI_PLUGIN_API_URL + _API_TOKEN

# ── Read (no special permission needed) ──────────────────────────────
state    = client.get_state()
history  = client.get_emotion_history(days=3)
memories = client.get_recent_memories(limit=5)
budget   = client.get_budget_summary()

# ── Read user_facts (requires read_user_facts: true) ──────────────────
facts = client.get_user_facts("sleep schedule", n=5, min_similarity=0.3)

# ── LLM call (requires llm: true) ────────────────────────────────────
reply = client.call_llm(
    "Summarise this: " + my_text,
    system="Be brief.",
    max_tokens=128,
)

# ── Queue a notification (requires output_mind_queue: true) ──────────
client.enqueue(
    kind="note",
    summary="Long sync has been running 3 hours",
    dedupe_key="sync:longrun:2026-04-10",
    priority="normal",
)

# ── Log (always available) ────────────────────────────────────────────
client.log("info", "Plugin finished", context={"ms": 142})

Error handling

python
try:
    facts = client.get_user_facts("sleep")
except PluginAPIError as e:
    if e.status == 0:
        # API server not reachable (Brain not started?)
        pass
    elif e.status == 401:
        # Token expired mid-run; degrade gracefully
        pass
    elif e.status == 403:
        # Permission not declared in PLUGIN.md
        pass
    else:
        raise
get_state()
GET /api/v1/plugin/state
Current mood, emotions, needs, user needs, last conversation time, and mind queue depth.
No special permission
get_emotion_history(days)
GET /api/v1/plugin/emotion-history
Recent emotion history rows with mood score, restlessness, and coherence. Up to 30 days.
No special permission
get_recent_memories(limit)
GET /api/v1/plugin/recent-memories
Most recent session memory summaries from ChromaDB memories collection (up to 20).
No special permission
get_budget_summary()
GET /api/v1/plugin/budget-summary
Today's LLM spend vs configured daily/monthly caps. Useful before calling call_llm().
No special permission
get_user_facts(query, n, min_similarity)
GET /api/v1/plugin/user-facts
Semantic search over ChromaDB user_facts. Returns matching fact texts and similarity scores.
read_user_facts: true
call_llm(prompt, system, model, max_tokens)
POST /api/v1/plugin/llm
Budget-enforced LLM call via Furoshiki's infrastructure. Raises PluginAPIError(429) if cap exceeded.
llm: true
enqueue(kind, summary, dedupe_key, …)
POST /api/v1/plugin/mind-queue
Insert one mind_queue item. Deduplicated by dedupe_key. Queued items reach the user via voice_dispatcher.
output_mind_queue: true
log(level, message, context)
POST /api/v1/plugin/log
Write a log line to log_store under source plugin:<name>. Visible in dashboard logs tab.
No special permission

Plugin-owned storage

Each plugin has a dedicated data directory created automatically at $FUROSHIKI_PLUGIN_DATA_DIR (~/.furoshiki/plugins/<name>/data/). Use it freely for caches, state, or a plugin-owned SQLite database.

python
import os, sqlite3

data_dir = os.environ['FUROSHIKI_PLUGIN_DATA_DIR']
db_path  = os.path.join(data_dir, 'cache.sqlite')

conn = sqlite3.connect(db_path)  # ✅ plugin's own DB — allowed
conn.execute('CREATE TABLE IF NOT EXISTS checks (ts TEXT, result TEXT)')
conn.execute('INSERT INTO checks VALUES (?, ?)', ('2026-04-10T09:00Z', 'ok'))
conn.commit()
Hard rule: Never open longmemory.sqlite, logs.sqlite, or any path constructed from FUROSHIKI_DATA. The static pre-screen in self_modification_dispatcher rejects generated plugins that contain these patterns, and the isolation prevents runtime access anyway.

Permissions reference

FlagUnlocksNotes
read_user_facts: trueget_user_facts()Semantic search over Chroma user_facts
output_mind_queue: trueenqueue() + stdout mediationItems reach the user via voice_dispatcher
llm: truecall_llm()Budget enforced; call tagged with plugin source
network: trueOutbound HTTP from subprocessDeclared intent; not kernel-enforced
openrouter_web: trueLLM :online modeSets FUROSHIKI_PLUGIN_OPENROUTER_WEB=1

All read endpoints and /log are available to any valid plugin token — no permission declaration needed.

Testing your plugin

Method 1 — hook-test-suite (recommended)

The reference hook-test-suite plugin exercises all 7 hook types and reports pass/fail. Install it, then use furoshiki plugins test-suite to validate the entire pipeline.

bash
# Install the reference test suite
cp -r examples/plugins/hook-test-suite ~/.furoshiki/plugins/

# Enable it
sed -i 's/active: false/active: true/' \
  ~/.furoshiki/plugins/hook-test-suite/PLUGIN.md

# Reload and run
furoshiki plugins reload
furoshiki plugins test-suite

# Inspect results
cat ~/.furoshiki/plugins/hook-test-suite/data/test-runs.jsonl

# Disable when done
sed -i 's/active: true/active: false/' \
  ~/.furoshiki/plugins/hook-test-suite/PLUGIN.md
furoshiki plugins reload

Method 2 — manual subprocess test

Issue a token manually, set env vars, and run your hook script directly without waiting for Brain to fire it.

bash
# Make sure Brain is running (so the API is live)
furoshiki start

# Issue a short-lived token (Python)
python3 - <<'EOF'
import sys; sys.path.insert(0, 'scripts')
from api_v1_routes import issue_plugin_token
token = issue_plugin_token('my-plugin', ['output_mind_queue'], timeout_sec=60)
print('export FUROSHIKI_PLUGIN_API_TOKEN=' + token)
EOF

# Set env and run the hook
export FUROSHIKI_PLUGIN_API_URL=http://127.0.0.1:7432/api/v1/plugin
export FUROSHIKI_PLUGIN_DIR=~/.furoshiki/plugins/my-plugin
export FUROSHIKI_PLUGIN_DATA_DIR=~/.furoshiki/plugins/my-plugin/data
python3 ~/.furoshiki/plugins/my-plugin/scripts/run.py

Checking results

bash
# View plugin logs in the dashboard or CLI
furoshiki logs plugin:my-plugin

# Check mind_queue for queued items
python3 -c "
import sys; sys.path.insert(0, 'scripts')
from db import get_conn
conn = get_conn()
rows = conn.execute(\"SELECT kind, summary, dedupe_key, status FROM mind_queue ORDER BY queued_at DESC LIMIT 10\").fetchall()
for r in rows: print(dict(r))
"

Self-modification path

Furoshiki can draft plugins autonomously via the delegation pipeline. Describe what you want in Telegram or via the inject API and let the system generate a proposal.

1
Describe the plugin
Tell Furoshiki: "Create a plugin that checks weather every morning and queues a note." The delegation classifier routes it to self_modification_dispatcher.
2
Creation + static pre-screen
The LLM drafts the plugin in a sandboxed pass. Before the review, a static pre-screen rejects any code containing sqlite3.connect(), longmemory.sqlite, or from db import.
3
LLM review
An independent review pass checks security, correctness, and alignment with the stated purpose. Confidence must be ≥ 0.75 to approve.
4
Proposal written
If approved, a proposal is written to ~/.furoshiki/proposals/ and a mind_queue notification is queued. You review it at your leisure.
5
Apply
furoshiki plugins apply <proposal-file> installs the approved plugin into plugins/ and reloads schedules.

Troubleshooting

SymptomCheck
Plugin missing from furoshiki plugins listIs the directory under $FUROSHIKI_DATA/plugins/? Is PLUGIN.md valid YAML with active: true?
Cron hook not runningDid you run furoshiki plugins reload after the edit? Check schedules.json for a plugin__<name> entry.
401 Invalid or expired plugin tokenHook ran longer than its timeout + 30 s grace. Increase timeout in PLUGIN.md.
403 Plugin permission not declaredAdd the required permission flag to permissions: in PLUGIN.md, then furoshiki plugins reload.
PluginAPIError(0, ...) connection refusedBrain is not running. Start it: furoshiki start.
Hook output ignored / bad_output in logsStdout must be valid JSON for hooks that require it. Check for stray print() before the JSON line.
ImportError: No module named 'db'Plugin is trying to import core Furoshiki modules. Replace with SDK calls. scripts/ is intentionally not on PYTHONPATH.
Self-mod draft rejected by pre-screenThe generated code contained sqlite3.connect() or from db import. Rephrase the request to not require direct DB access.