jind-ai

module
v0.9.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 17, 2026 License: MIT

README

English | 日本語

jind-ai

A CLI tool for running and managing multiple agent sessions simultaneously (Claude Code is the first-class citizen; other agents plug in via internal/agent/<kind>/).

Image

https://github.com/user-attachments/assets/62e9d64a-aa7d-42f8-8edf-03f724fe0ee4

Supported agents

Kind CLI Notes
claude (default) Claude Code 2.x First-class support. Uses --session-id / --resume and Claude Code's native hook system for state tracking.
codex OpenAI Codex CLI 0.144+ Hooks are injected per-invocation via -c hooks.X=[...]; grant trust once through the /hooks dialog on first launch (see docs/gotchas.md). Session name / resume UUID are learned from Codex's SessionStart hook payload (no --session-id equivalent upstream yet).

Select a non-default adapter per session:

jin session new --agent codex --workdir ~/repos/myrepo

Or set a persistent default via default_agent: codex in ~/.config/jind-ai/config.yaml.

The TUI create form includes an agent picker step whenever more than one adapter is registered — pick the kind per session with ↑↓/j/k + Enter. Initial selection is resolved as --agent > default_agent > "claude". Use jin ui --agent codex to preselect Codex for this TUI run only (config is left untouched):

jin ui --agent codex   # transient default; ends when TUI exits

Features

  • Multi-session management: Run multiple Claude Code sessions in the background simultaneously
  • tmux-native: Each session runs in its own tmux pane, so your existing ~/.tmux.conf, custom keybindings, status bar, and copy-mode setup work as-is
  • Decoupled UI / logic architecture: All session management, state transitions, and hook handling live in the daemon. The TUI is a thin client that talks to the daemon over a Unix socket and holds no session-management logic. In principle any alternate UI (web, editor extension, ...) can drive the same IPC (see docs/architecture.md / docs/ipc-protocol.md)
  • TUI: Interactive terminal UI for listing, monitoring, and operating sessions
  • Attach/Detach: Quickly switch between sessions (Ctrl+] to detach)
  • Real-time status tracking: Live display of working directory, branch, and latest message
  • Session filter & Paging: / opens a fuzzy-search popup over session name, directory, branch, fleet, and agent kind
  • Plugins: Run your own shell-executable plugins on session status changes or on demand — for example, desktop notifications via jin plugin install jind-ai-notifier (registry name; git URLs and local --link paths are also supported)

Installation

Download from GitHub Releases

Download the binary for your OS/architecture from the Releases page.

# Example: Linux amd64
curl -Lo jind-ai.tar.gz https://github.com/takaaki-s/jind-ai/releases/latest/download/jind-ai_0.7.0_linux_amd64.tar.gz
tar xzf jind-ai.tar.gz
sudo mv jin /usr/local/bin/
Go install
go install github.com/takaaki-s/jind-ai/cmd/jin@latest
Build from source
git clone https://github.com/takaaki-s/jind-ai.git
cd jind-ai
make build    # Build to bin/jin
make install  # Install to $GOPATH/bin

Quick Start

1. Start the daemon
jin daemon start
2. Launch the TUI
jin ui
3. Create and attach to a session

Press n in the TUI to create a session, then Enter to attach.

Press Ctrl+] to detach and return to the TUI.

Session Status

Session states are detected via Claude Code hooks in an event-driven manner.

Status Icon Detection Description
thinking UserPromptSubmit hook Processing
permission ? Notification hook Awaiting permission
running Internal Running
creating + Internal Creating (CC starting up)
idle Stop hook Waiting for input
stopped Process death detection Stopped

CLI Commands

Daemon management
jin daemon start   # Start daemon
jin daemon stop    # Stop daemon
jin daemon status  # Check status
Session management
# Create session (interactive via TUI - recommended)
jin session new

# Create session (specify working directory)
jin session new --workdir ~/repos/myrepo

# List sessions
jin session list

# List sessions in JSON format (for scripting / LLM integration)
jin session list --json

# Attach to a session
jin session attach <session-name>

# Get session details
jin session info <session-name>

# Send a prompt to a session
jin session send <session-name> "your prompt here"

# Wait for a session to become idle (default timeout: 300s)
jin session wait <session-name>
jin session wait <session-name> --timeout 60

# Get the last assistant message
jin session output <session-name>

# Get the last N conversation pairs
jin session output <session-name> --last 3

# Kill a session
jin session kill <session-name>

# Delete a session
jin session delete <session-name>

# Bulk delete stopped sessions
jin cleanup stopped
jin cleanup stopped --dry-run   # Preview what will be deleted

Aliases: session can be shortened to sess (e.g., jin sess list). list to ls, delete to rm.

LLM API (scripting / automation)

The following commands support --json for structured output, enabling integration with scripts and other LLM agents.

# All session commands support --json
jin session list --json
jin session new --workdir ~/repos/myrepo --json
jin session info <session-name> --json
jin session kill <session-name> --json

# Send a prompt and wait for completion
jin session send <session-name> "fix the failing test" --json
jin session wait <session-name> --timeout 120 --json
jin session output <session-name> --json

# Pipeline example: send a prompt, wait, get output
jin session send my-session "refactor main.go"
jin session wait my-session --timeout 300
jin session output my-session --last 1
Orchestration: parent Claude driving child sessions

For richer orchestration (e.g. a parent Claude that needs to inspect what a child actually did, not just the assistant's text), use session result. It returns structured tool_use / tool_result / thinking blocks parsed directly from the Claude Code transcript JSONL — no tmux pane scraping, no truncation by scrollback buffer.

# Send a prompt, wait until the child stops (idle OR waiting on permission),
# then fetch what it actually did.
jin session prompt my-session "run go test ./... and report failures"
jin session wait my-session --until idle,permission --timeout 600
jin session result my-session --json | jq '.entries[].blocks[] | select(.kind=="tool_result")'

# Incremental fetch: only entries after a checkpoint.
# --since is strictly exclusive: pass the last entry's timestamp to receive
# only entries that came after it (no duplicates). Claude Code emits timestamps
# with millisecond precision (e.g. "2026-04-09T13:23:10.456Z").
T1=$(jin session result my-session --json | jq -r '.entries[-1].timestamp')
jin session prompt my-session "now also run go vet"
jin session wait my-session --until idle,permission
jin session result my-session --since "$T1" --json

# Filter to a specific tool, or to errors only
jin session result my-session --tool Bash --json
jin session result my-session --errors-only --json

prompt is an alias for send.

Exit codes
Code Meaning
0 Success
1 General error
2 Session not found
3 Daemon not running
4 Timeout (session wait)
Utilities
jin session workdir <session-name>    # Print session's working directory path
jin session edit <session-name>       # Open session's working directory in EDITOR

The following shell functions are useful:

# cd to a session's working directory
cc-cd() { cd "$(jin session workdir "$1")"; }

# Select a session with fzf and cd to its working directory
cc-cdf() {
  local session
  session=$(jin session list | tail -n +2 | fzf --height 40% --reverse | awk '{print $1}')
  [[ -n "$session" ]] && cd "$(jin session workdir "$session")"
}

# Select a session with fzf and attach
cc-attach() {
  local session
  session=$(jin session list | tail -n +2 | fzf --height 40% --reverse | awk '{print $1}')
  [[ -n "$session" ]] && jin session attach "$session"
}
Shell Completion
# bash
source <(jin completion bash)

# zsh
source <(jin completion zsh)

# fish
jin completion fish | source

Configuration

jind-ai follows the XDG Base Directory Specification. Files are split across config / state / runtime directories:

$XDG_CONFIG_HOME/jind-ai/      (default: ~/.config/jind-ai)
└── config.yaml                # Configuration file

$XDG_STATE_HOME/jind-ai/       (default: ~/.local/state/jind-ai)
├── state.yaml                 # State file (last used repository, etc.)
├── sessions/                  # Session data
├── hooks-settings.json        # Generated hooks settings (auto-managed)
├── plugins.lock.yaml          # Installed-plugin ledger (see Plugins below)
├── plugin-logs/               # Per-plugin dispatch/run and build output
├── daemon-debug.log           # Daemon debug log (when JIN_DEBUG=1)
├── hook-debug.log             # Hook debug log (when JIN_DEBUG=1)
└── plugin-debug.log           # Plugin dispatcher debug log (when JIN_DEBUG=1)

$XDG_DATA_HOME/jind-ai/        (default: ~/.local/share/jind-ai)
└── plugins/                   # Installed plugins (see Plugins below)

$XDG_RUNTIME_DIR/jind-ai/      (fallback: $TMPDIR/jind-ai-<uid>)
└── daemon.sock                # Daemon socket
Example configuration (~/.config/jind-ai/config.yaml)
# Customize keybindings (defaults are used when omitted)
keybindings:
  # Session list view
  up: ["up", "k"]
  down: ["down", "j"]
  attach: ["enter"]
  new: ["n"]
  kill: ["x"]
  delete: ["d"]
  refresh: ["r"]
  search: ["M-f"]         # opens the session filter popup (fuzzy search).
                          # Default M-f (Alt+f). Must be modifier-prefixed —
                          # a bare letter is consumed by the display pane and
                          # never reaches the outer tmux binding.
                          # Use ["/"] to restore the pre-M-f bare-slash key
                          # (breaks agent slash-commands in the display pane).
  vscode: ["v"]
  quit: ["q", "ctrl+c"]
  help: ["?"]
  # Session creation form
  next_field: ["tab"]
  prev_field: ["shift+tab"]
  submit: ["enter"]
  cancel_form: ["esc"]
  # While attached
  detach: ["ctrl+]"]  # Default: ctrl+]
                       # Supported keys: ctrl+^, ctrl+], ctrl+\, ctrl+g
  # Outer tmux (jin-mgr) — action palette trigger, both panes
  action_panel: ["M-p"]  # Default: M-p
                          # action_panel: []           to disable
                          # action_panel: ["M-x"]      to rebind
  # Outer tmux (jin-mgr) — per-plugin action triggers (both panes)
  # No default — user opts in per plugin. Fires `jin plugin run <name>`
  # via tmux `run-shell -b` (background, no output to the active pane), so
  # the plugin itself is responsible for opening any popup (matches the
  # `jin pane popup --here` model). Uninstalled plugins are silently
  # skipped with one log line. Key collisions with core outer-tmux
  # bindings are warned only; tmux last-write-wins.
  # Outer-tmux keys accept both tmux notation (`M-n`, `C-f`) and the "+"
  # style (`alt+n`, `ctrl+f`); they are normalized to tmux form at load.
  plugins:
    # notifier:         { keys: ["M-n"] }        # 1 打鍵で通知一覧
    # worktree-cleanup: { keys: ["M-w", "M-c"] } # 複数キー可
    # journal:          { keys: ["ctrl+f"] }     # "+" style も可

# Optional: popup sizes (percent, int 1-100). Every entry is optional;
# omitted popups keep their default (create/session_filter/action = 70-80).
# See docs/tui-guide.md#popup-sizes for the full table and delivery paths.
popups:
  create:         { width: 80, height: 80 }
  session_filter: { width: 70, height: 70 }
  help:           { width: 60, height: 60 }
  action:         { width: 70, height: 70 }
  plugin_default: { width: 70, height: 70 }
  plugins:                                # per-plugin overrides
    # my-notifier:  { width: 40, height: 20 }
Worktree placement

By default, jin session new --worktree creates worktrees under $XDG_STATE_HOME/jind-ai/worktrees/{name} (typically ~/.local/state/jind-ai/worktrees/). Override this with worktree.base_dir in config.yaml:

worktree:
  # Group worktrees per repository under a stable location
  base_dir: "${HOME}/ghq/worktrees/{repo}/{name}"

Other common layouts:

# Sibling directory next to each repo checkout
worktree:
  base_dir: "${HOME}/dev/worktrees/{name}"

# Under a fixed root, ignoring repo name
worktree:
  base_dir: "/mnt/fast/worktrees/{name}"

Template variables:

Placeholder Expands to
{name} Worktree name (e.g. jin-abcd1234, or the --name you passed)
{repo} Basename of the original repository
${VAR} Environment variable (os.ExpandEnv semantics)

The expanded path must be absolute. Unknown {xxx} placeholders are rejected at session creation.

Worktree branch naming

Every worktree gets a companion branch. Two worktree: settings control how it is named:

worktree:
  branch_prefix: "topic/"   # Default: "jin/". Use "" to drop the prefix.
  default_branch: "main"    # Fallback base branch. Default: "" (no fallback).
  • branch_prefix — prepended to the auto-derived worktree name to form the branch name. The leading jin- on the worktree name is stripped first, so under the default jin-abcd1234 becomes jin/abcd1234 (not jin/jin-abcd1234). Ignored when you pass --worktree-branch <name> to jin session new, since that overrides the branch outright.
  • default_branch — used only when jind-ai cannot auto-detect the repository's default branch. Detection reads refs/remotes/origin/HEAD; local clones that never had it set (some tarballs, git clone --no-checkout, older clones) will hit the fallback. If detection fails and default_branch is empty, session creation errors with cannot detect default branch.

Worktree creation itself is offline — the new branch is cut from your local origin/<base> with no network round-trip, so heavy repos aren't taxed on every session. If you want the worktree to start from the freshest remote tip, git fetch origin <base> in the source repo before running jin session new --worktree, or wire the fetch into the post-create hook below.

TUI Keybindings

Session list view
Key Action
↑/k Move up
↓/j Move down
←/h Previous page
→/l Next page
M-f Open session filter (fuzzy popup) — see Outer tmux — session filter
Enter Attach to session
n Create new session
x Kill session
d Delete session
r Refresh list
v Open in VS Code
? Show help
q Quit
Session creation form
Key Action
Tab Move to next field
Shift+Tab Move to previous field
Enter Create session
Esc Cancel

While attached, press Ctrl+] (default) to detach and return to the TUI. You can change this in config.yaml under keybindings.detach.

Supported detach keys:

Key Description
ctrl+] Default
ctrl+^ Ctrl+Shift+6
ctrl+\ Ctrl+Backslash
ctrl+g Ctrl+G
Outer tmux — action palette

M-p (Alt+p, default) opens the action palette, a searchable popup listing every built-in TUI action plus installed plugin actions. It's bound at the outer tmux (jin-mgr) root key table, so it fires the same way whether the session list (left) or an attached agent (right) has focus.

Override or disable it in config.yaml (see keybindings.action_panel above):

keybindings:
  action_panel: ["M-x"]  # rebind to Alt+x
  # action_panel: []       # disable entirely (no bind-key issued)

Keys must include a modifier (M-/C-) — a bare letter would be consumed as normal input by the agent in the right pane instead of reaching the outer tmux binding.

Outer tmux — session filter

M-f (Alt+f, default) opens the session filter, a fuzzy-search popup for jumping straight to a session: type a few characters and press Enter to attach immediately. It's bound the same way as the action palette above — at the outer tmux (jin-mgr) root key table, so it fires from either pane. Matched fields are session description, working directory, current working directory, git branch, fleet, and agent kind (subsequence matching via sahilm/fuzzy, smart-case, ranked by score). Esc closes the popup without changing anything; / or Ctrl+P/Ctrl+N move the cursor.

The default changed from / to M-f because a bare-letter binding at the outer tmux root also swallows / typed in the display pane, breaking agent slash-commands (Claude Code /help, less/vim /search, etc.). The action palette entry ("session filter") also invokes this popup, so you can reach it without a shortcut at all.

Override or disable the trigger the same way as action_panel:

keybindings:
  search: ["ctrl+p"]      # rebind to Ctrl+p
  # search: ["/"]         # restore pre-M-f bare-slash (breaks display-pane `/`)
  # search: []            # disable entirely (no bind-key issued)

Claude Code Hooks

jind-ai uses Claude Code hooks to detect session state changes. Hooks are configured automatically — no manual setup required.

When a session starts, jind-ai generates $XDG_STATE_HOME/jind-ai/hooks-settings.json (default ~/.local/state/jind-ai/hooks-settings.json) and passes it to Claude Code via claude --settings. This file wires up the following hooks:

Hook Event Role
UserPromptSubmit User submits a prompt → set session to thinking
PostToolUse Tool execution ends → set session to thinking (recovers from permission state)
Stop Claude's turn ends → set session to idle (dispatches a task-complete JIN_NOTIFY_KIND to plugins)
Notification Permission request, etc. → set session to permission (dispatches a permission JIN_NOTIFY_KIND to plugins)

Worktree Post-Create Hook

When you create a session with jin session new --worktree, jind-ai can run a setup script right after the worktree is created — installing dependencies, copying .env, initializing submodules — so every new worktree lands ready to use without any manual steps.

Script location

Place a shell script at .jin/worktree-post-create.sh in the original repository (not the worktree). It always runs via bash, so chmod +x is not required. If the file doesn't exist, the hook is silently skipped.

#!/usr/bin/env bash
set -euo pipefail

# Copy .env from parent repository (git-ignored)
cp "$JIN_REPO_ROOT/.env" "$JIN_WORKTREE_PATH/.env" 2>/dev/null || true

# Install dependencies
pnpm install
Environment variables
Variable Description
JIN_WORKTREE_PATH Absolute path of the newly created worktree
JIN_WORKTREE_BRANCH Branch checked out in the worktree
JIN_WORKTREE_BASE Base branch the worktree was created from
JIN_SESSION_ID UUID of the session being created
JIN_SESSION_NAME Session name, if one was given via --name (empty otherwise — the auto-derived name isn't assigned until after the hook runs)
JIN_REPO_ROOT Absolute path of the original repository
Security: allowlist

Since the script is checked into a repository, jind-ai never runs it unless the repository has been explicitly trusted (a direnv-style allow model). Trust is tracked by the script's SHA256 — editing the script requires trusting it again.

jin worktree allow    # Trust the current repository (shows the script, asks for confirmation)
jin worktree revoke   # Revoke trust
jin worktree status   # Show the allow status of the current repository
jin worktree list     # List all trusted repositories

If the script exists but isn't trusted (or changed since it was trusted), the hook is skipped with a warning — the worktree is still created and Claude still starts normally. When creating from the TUI, the popup surfaces a three-way prompt (a: Allow, s: Skip and create anyway, c: Cancel) so you can decide without dropping to a shell.

Skipping the hook
  • jin session new --worktree --no-hook — skip the hook for this session only
  • worktree.hook_enabled: false in ~/.config/jind-ai/config.yaml — disable the hook for all repositories
  • worktree.hook_timeout: <seconds> — change the timeout (default: 300). On expiry the hook's process group is sent SIGTERM, given a 5-second grace period, then SIGKILL if still alive.
On failure

If the hook exits non-zero or times out, the worktree and its branch are rolled back and jin session new fails with a non-zero exit code. The hook's stdout/stderr are kept at ~/.local/state/jind-ai/hook-logs/<session-id>.log for troubleshooting, even after a rollback.

Plugins

jind-ai can run your own shell-executable plugins in reaction to session status changes, or on demand. A plugin is a directory with a manifest and an entry-point script; jind-ai never inspects what the script does, only when it runs and what environment it gets.

Community plugins are discoverable through the plugin registry: jin plugin ls-remote lists them, jin plugin install <name> installs one by registry name with a commit-pinned consent screen.

Two ways a plugin runs
  • Event listener — a manifest action subscribes to status_changed via its on: matcher. Good for notifications, logging, CI triggers — anything non-interactive. Note: an event fires only when the status actually changes; a notification without a status transition (e.g. a repeated stop while already idle) does not dispatch. If a plugin declares multiple actions, each is matched and debounced independently, so the same event can fan out to several actions on the same plugin.
  • Action — launched explicitly with jin plugin run <name> [action] [--session <selector>]. Good for interactive workflows (e.g. a popup-based diff review UI). Omit [action] to run the plugin's default action (actions[0]); pass an action ID to select a specific one. Set an action's on: [] to make it action-only. Without --session the run is a global action: all session-derived env vars are empty. On every action run — global or session-scoped — JIN_CALLER_TMUX_SOCKET / JIN_CALLER_TMUX_PANE identify where the invoking CLI was launched from, when it sat inside a tmux client.

Both entry points execute the same action entrypoint with the same environment; only the trigger differs.

Manifest (jind-ai-plugin.yaml)

Place this file at the root of the plugin directory. The same manifest is read at runtime (dispatcher) and at publish time (registry crawler); one file, one source of truth.

schema_version: 2
name: notifier
version: 0.2.0
description: Desktop notifications for jin sessions
license: MIT
homepage: https://github.com/foo/notifier
jin: ">=0.8.0"
install:
  source:
    build:
      - go build -o bin/notifier ./cmd/notifier
    entrypoint: ./bin/notifier
actions:
  - id: default                                      # actions[0] is the implicit default
    entrypoint: ./bin/notifier notify
    on: ["status_changed:idle", "status_changed:permission"]
    label: "Desktop notification"
    timeout: 30s
  - id: send-dm                                      # `jin plugin run notifier send-dm`
    entrypoint: ./bin/notifier send-dm
    on: []                                           # action-only (no event subscription)
    label: "Send DM to teammate"
    popup: { width: 60, height: 30 }

Existing v1 manifests (schema_version: 1 with top-level entrypoint / on / timeout / popup) keep working: they are normalised at parse time into a single-action shape, so no author-side migration is required. Write new manifests as v2.

Field Required Description
schema_version Yes Manifest generation. 1 or 2. v1 is auto-normalised to a single-action v2 shape at parse time
name Yes [a-z][a-z0-9-]{1,63}; unique in the registry; must match the directory name jind-ai installs it under
version Yes Plugin's own semver (X.Y.Z); pre-release/build metadata allowed
description Yes One-liner shown in jin plugin ls-remote search results
license / homepage No Optional metadata carried into the registry entry
jin Yes Semver constraint on the jin binary (">=0.8.0", "^0.8", ">=0.8 <0.10"). Checked at install and at every dispatch
install.source.build No Ordered list of build commands (each element runs in its own bash -c, no piping across elements) — see Language-specific guidance. Omit for plugins that ship a directly-executable entrypoint (shell scripts, prebuilt binaries in the repo)
install.source.entrypoint Conditional Default entrypoint the dispatcher executes if a specific action does not declare its own. Required when install.source is used
install.release_asset.pattern Conditional Alternative to install.source. Downloads a prebuilt asset from the latest GitHub Release. Placeholders: {os}, {arch}
actions[] v2 only List of actions the plugin exposes. actions[0] is the implicit default. Each element carries id / entrypoint / on / label / timeout / popup (fields below)
actions[].id Yes [a-z][a-z0-9-]{0,63}; unique within the plugin. Explicit IDs are strongly recommended — palette, keybindings, and jin plugin run all reference actions by ID
actions[].entrypoint Conditional Path (relative to the plugin dir) executed for this action. May be omitted when install.source.entrypoint covers it
actions[].on No Per-action status_changed matchers, same syntax as v1's top-level on. Empty or omitted = action-only. Matching / debouncing is independent per action
actions[].label No Human-readable label shown in the palette and help popup. When empty, the palette displays <plugin>:<action-id> (or just <plugin> when the default action's ID is default)
actions[].timeout No Per-action override for timeout; default 30s
actions[].popup.width / .height No Per-action manifest hint for jin pane popup --here size (1–100, percent of terminal)
actions[].listener No Marks the action as an event-only endpoint. It still fires on matching on: events, but is hidden from every user-facing surface (palette, help popup, shell completion). Direct invocation via jin plugin run <plugin> <action> remains available for debugging. Requires a non-empty on: — a listener with no events has no runtime purpose
on / timeout / popup (top-level) v1 only Legacy v1 fields; forbidden in v2 (validation error). Put them under actions[] instead

install.source and install.release_asset are mutually exclusive.

config.yaml only enables/disables plugins and tunes dispatch timing (below) — it never duplicates manifest fields.

Listener actions are the common pattern for a plugin that both reacts to events and exposes a UI. Split the two concerns so only the UI part appears in the palette:

actions:
  - id: list                        # user-facing: appears in the palette
    entrypoint: ./notifier.sh list
    label: "Show pending sessions"
  - id: listen                      # event listener: hidden from the palette
    entrypoint: ./notifier.sh listen
    on: ["status_changed"]
    listener: true                  # requires a non-empty on:
What a plugin receives

Environment variables:

Variable Description
JIN_EVENT status_changed or action
JIN_ACTION_ID ID of the manifest action that fired this run (default for v1 manifests / v2 default actions synthesised as default). A shared entrypoint script can dispatch on this instead of parsing argv
JIN_SESSION_ID Session ID
JIN_STATUS Current status
JIN_PREV_STATUS Previous status (empty for an action run)
JIN_AGENT_KIND Adapter kind (claude, ...)
JIN_WORKDIR Session's working directory
JIN_TMUX_PANE_ID tmux pane ID, if known
JIN_NOTIFY_KIND Notification kind for this transition: task-complete, error, permission, or empty when the transition triggers no notification
JIN_PLUGIN_DEPTH Chain depth — see Constraints
JIN_SOCKET Daemon socket path; the jin CLI a plugin invokes picks this up automatically
JIN_BIN Absolute path of the daemon's own jin binary. Prefer "${JIN_BIN:-jin}" over a bare jin — a jin found on PATH may be an older install that lacks newer subcommands
JIN_CALLER_TMUX_SOCKET Action runs only: socket path of the tmux server the invoking CLI ran inside (from its $TMUX). Unset — not empty — when the caller was outside tmux
JIN_CALLER_TMUX_PANE Action runs only: the invoking CLI's pane ID (from its $TMUX_PANE). Unset when unknown

The same data is also written to stdin as JSON (same fields, snake_case; caller tmux context is env-only).

For anything beyond this thin payload, call back into jind-ai:

jin session info "$JIN_SESSION_ID" --json    # full session details
jin session send "$JIN_SESSION_ID" "..."     # send a prompt
jin session result "$JIN_SESSION_ID" --json  # structured transcript entries
jin session focus "$JIN_SESSION_ID"          # make the running TUI display this session
jin pane popup "$JIN_SESSION_ID" -- <cmd>    # tmux popup over the session's pane
jin pane popup --here -- <cmd>               # tmux popup over the caller's own pane (uses $TMUX, falling back to JIN_CALLER_TMUX_SOCKET)
jin pane split "$JIN_SESSION_ID" -- <cmd>
jin pane capture "$JIN_SESSION_ID"
jin pane send-keys "$JIN_SESSION_ID" <keys>

Compatibility contract: treat any environment variable, JSON field, or CLI flag you don't recognize as something to ignore, not an error. jind-ai only adds to this surface within a schema_version; breaking removals happen across a schema_version bump (or, pre-1.0, across a minor jin release).

Install / update / remove / list
# From the plugin registry (see docs/plugin-registry.md)
jin plugin ls-remote                              # list plugins in the registry
jin plugin install jind-ai-notifier               # latest release; `plugin update` follows the plugin
jin plugin install jind-ai-notifier -v 0.2.0      # pin a specific version; `plugin update` will not move it
jin plugin install jind-ai-notifier --force       # override an unsatisfied jin compat range

# From a git source (github.com/, gitlab.com/, self-hosted, ssh URLs, ...)
jin plugin install github.com/owner/repo          # default branch; `plugin update` follows highest semver tag
jin plugin install github.com/owner/repo@v1.2.0   # pinned to a tag/branch/SHA; `plugin update` will not move it

# From a local directory, symlinked in place (development)
jin plugin install --link ./my-plugin

jin plugin update <name>
jin plugin remove <name>
jin plugin list          # NAME / VERSION / STATE / SOURCE; --json for scripting

# Validate a manifest — same checks the registry crawler runs
jin plugin validate                               # defaults to .
jin plugin validate --github-actions              # emit ::error / ::warning annotations

A git install/update shows the manifest (name, version, on, entrypoint, build) and the commit SHA it resolved to, and asks for confirmation (--yes to skip) before touching anything; the approved commit SHA is recorded in plugins.lock.yaml, so a later install/update never silently lands on a different commit than the one you saw. A --linked plugin skips this — linking a local path is itself the trust decision, and jind-ai never runs build: for a linked plugin.

jin plugin update <name> resolves the plugin's latest release for an unpinned install: registry names resolve to the registry's declared latest_version, and raw git-URL installs pick the highest semver tag from git ls-remote --tags (falling back to the locked ref when the remote advertises no semver tags). A pinned install — one that used -v <ver> on the registry path or @<ref> on the git-URL path — is a no-op with a message pointing at reinstall as the way to move it. This mirrors install: the "give me latest" and "hold this exact ref" intents are decided once at install time and honoured by every later update.

Language-specific guidance
  • Shell / single file — commit the script, point entrypoint at it, and omit install.source.build; add a chmod +x step only if the script is generated or the exec bit isn't preserved in git.
  • Node.js / TypeScript — bundle to dist/ (esbuild etc.) as one build step; resolving dependencies at runtime (bun/deno) works too, but that first-dispatch network fetch can fail silently since dispatch is fail-open — a pre-built bundle is more predictable.
  • Go / Rust / other compiled languages — declare a build sequence under install.source.build; each element runs as its own process (no shell piping between elements) so the binary matches the user's platform/arch (and go.sum / Cargo.lock give reproducibility). Builds run once per install/update; jind-ai does not resolve dependencies or detect a toolchain for you — document what's required in your plugin's own README. A non-zero exit fails the install/update atomically (nothing is left half-installed), with output kept at ~/.local/state/jind-ai/plugin-logs/<name>-build.log. jind-ai injects npm_config_ignore_scripts=true into the build environment by default (a supply-chain guard you can override inside your own build step); the build itself runs with your own user privileges — it is not sandboxed.
Constraints
  • No persistent processes. jind-ai runs a plugin per event/action and tears it down; don't build a long-running daemon into entrypoint. If you need one, run it yourself (manually, or as a systemd user unit) and keep the plugin a thin per-event client to it (e.g. curl).
  • Popups don't inherit JIN_* env vars. jin pane popup / jin pane split run their command in a process tmux spawns fresh — pass any data the popup needs as arguments on its command line (or as env-assignment prefixes in the command string, e.g. jin pane popup "$JIN_SESSION_ID" -- "JIN_BIN=$JIN_BIN inner.sh --id $JIN_SESSION_ID"), not as inherited env vars.
  • Fail-open. A plugin that errors, times out, or hangs never blocks a session's status pipeline — it's logged and the pipeline moves on. Timeout defaults to 30s (timeout: in the manifest).
  • Loop residual risk. jind-ai debounces repeated dispatch of the same (plugin, session, event) within a short window (default 3s, plugins.debounce below) and rejects a plugin chaining another plugin run beyond one hop (JIN_PLUGIN_DEPTH). Neither catches a slow ping-pong (e.g. a plugin that sends a prompt whose eventual response re-triggers the same plugin a few seconds later) — avoiding that is on the plugin author.
Config (~/.config/jind-ai/config.yaml)
plugins:
  enabled: true          # default true; false disables all plugin dispatch
  disabled: ["notifier"] # disable individual plugins by name
  build_timeout: 300  # seconds, install/update build step (default 300)
  debounce: 3          # seconds, dispatch debounce window (default 3)
Compatibility

Plugins declare a semver constraint on the jin binary via jin: (e.g. ">=0.7.0", "^0.7"). Checked at install/update (fail-closed — a plugin outside the range is rejected before anything is written) and again at every dispatch (fail-open — an incompatible installed plugin is skipped, logged once, and shown as incompatible in jin plugin list, with jin plugin run pointing you at jin plugin update <name>). Development builds (jin --version reporting dev or an unstamped value) satisfy every constraint so local plugin work is unblocked.

The schema_version field is orthogonal to jin: it identifies the manifest generation. jind-ai supports schema versions in a window [min, current], currently both 1, and older schemas will be honoured up to two generations back once we start bumping.

Debugging a plugin
export JIN_DEBUG=1
tail -f ~/.local/state/jind-ai/plugin-debug.log        # dispatcher decisions
tail -f ~/.local/state/jind-ai/plugin-logs/<name>.log  # a plugin's own stdout/stderr

Debugging

# Enable debug logging
export JIN_DEBUG=1

# Start daemon
jin daemon start

# View logs
tail -f ~/.local/state/jind-ai/daemon-debug.log

Requirements

  • Go 1.24.5+
  • tmux 3.3+
  • Claude Code CLI installed

License

MIT

Directories

Path Synopsis
cmd
jin command
internal
action
Package action defines the action palette's data model: the built-in ("core") actions the TUI exposes plus the dynamic set contributed by installed plugins.
Package action defines the action palette's data model: the built-in ("core") actions the TUI exposes plus the dynamic set contributed by installed plugins.
agent
Package agent hosts the Agent interface and the process-global registry that maps a kind string (e.g.
Package agent hosts the Agent interface and the process-global registry that maps a kind string (e.g.
agent/agenttest
Package agenttest exposes helpers for tests that need to drive the agent registry deterministically (register a stub, swap the current entry, wipe state between tests).
Package agenttest exposes helpers for tests that need to drive the agent registry deterministically (register a stub, swap the current entry, wipe state between tests).
agent/claude
Package claude is the Claude Code adapter.
Package claude is the Claude Code adapter.
agent/codex
Package codex is the OpenAI Codex CLI adapter.
Package codex is the OpenAI Codex CLI adapter.
agent/register
Package register wires every known adapter into the agent registry via its init function.
Package register wires every known adapter into the agent registry via its init function.
exitcode
Package exitcode defines exit code constants and the ExitError type for CLI commands.
Package exitcode defines exit code constants and the ExitError type for CLI commands.
git
Package git wraps the git CLI for the operations jind-ai needs (worktree management, branch detection).
Package git wraps the git CLI for the operations jind-ai needs (worktree management, branch detection).
paths
Package paths resolves jind-ai's data directories according to the XDG Base Directory Specification.
Package paths resolves jind-ai's data directories according to the XDG Base Directory Specification.
plugin
Package plugin owns the runtime side of jind-ai plugins — installation, registry classification, event dispatch, and per-run execution.
Package plugin owns the runtime side of jind-ai plugins — installation, registry classification, event dispatch, and per-run execution.
transcript
Package transcript provides reading functionality for Claude Code transcript files.
Package transcript provides reading functionality for Claude Code transcript files.
tui
worktreehook
Package worktreehook discovers, verifies, and executes the optional .jin/worktree-post-create.sh script that runs after a git worktree is created for a new session.
Package worktreehook discovers, verifies, and executes the optional .jin/worktree-post-create.sh script that runs after a git worktree is created for a new session.
pkg
plugin/manifest
Package manifest is the single source of truth for jind-ai plugin manifests as they appear both at runtime (the dispatcher reads them) and in the plugin registry ecosystem (the crawler and the CLI's install command read them).
Package manifest is the single source of truth for jind-ai plugin manifests as they appear both at runtime (the dispatcher reads them) and in the plugin registry ecosystem (the crawler and the CLI's install command read them).

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL