claude-ops
claude-ops is a set of small tools for people who run many Claude Code chats on one Mac at the same time, in different project folders. It finds any chat from any folder, sends a message into a chat that is already running, starts chats inside tmux so they can be reached by name, wakes a chat from a scheduled job when something new happens, and adds a few hooks and skills that came out of daily use.
Everything here started as scripts I wrote for my own machine. I made them general and tested before publishing them.
Install
pip install claude-ops
It needs Python 3.9 or newer and nothing else from PyPI. Sending into iTerm2 uses AppleScript, so that part works only on macOS. The tmux parts work wherever tmux does.
Find any chat
Claude Code stores every chat under ~/.claude/projects, and claude --resume only sees the chats of the folder you are in.
claude-ops sessions # all chats from every folder, newest first
claude-ops sessions login # filter by title, first message, folder or branch
claude-ops search "pg_restore" # search the full text of every chat (uses ripgrep if it can find it)
Each result prints the line that resumes it (cd <folder> && claude --resume <id>). The index is cached in ~/.cache/claude-ops/sessions.json and only changed files are read again, so a later run over twenty thousand chats takes a second or two. The time shown is the last message in the chat, not the file's modification time, because backup and restore tools give every file a new time. A chat is marked "maybe running" when it was active in the last 15 minutes and a claude process is running in its folder. Those are two separate facts, so it says "maybe".
Send a message into a running chat
claude-ops inject --list # reachable iTerm2 sessions, and which run Claude Code
claude-ops inject --target my-chat --text "please run the tests again"
claude-ops inject --target 0A1B2C3D-1111-2222-3333-444455556666 --file prompt.txt
claude-ops inject --transport tmux --target claude-build --text "..."
A target is an iTerm2 session id, part of a tab name, or a tmux session name. Claude Code rewrites tab names, so a job that runs for a long time should keep the session id. Text with several lines is sent as one bracketed paste, so it arrives as one message instead of one message per line.
Before it sends, claude-ops checks that the target is running Claude Code. Text sent into a plain shell would run as a command, so when the check is not sure it refuses (exit code 4). Exit code 3 means the chat is not open.
Every send is written to ~/.local/state/claude-ops/inject.jsonl before it happens, and its result after, with the same id on both lines. An attempt with no result line means the sender stopped in between. The log keeps the length and a SHA-256 hash of the text, not the text, unless you pass --keep-text.
Start chats inside tmux
claude-ops chat new build --cwd ~/code/app --trust # starts "claude -n build" in tmux session claude-build
claude-ops chat show build # opens it in an iTerm2 window (tmux -CC)
claude-ops chat list
A chat in tmux keeps running when its window closes, and other tools can reach it by its tmux name from the first second. A few things I learned while building this.
- The first run in a new folder stops at "Do you trust the files in this folder?". That prompt lists "No, exit" first with the cursor on it, so pressing Enter would close the chat. With
--trust, claude-ops waits until the screen is still, moves to "Yes, I trust this folder", checks that the cursor is really there, and only then presses Enter. - If the chat stops at another prompt (sign in, permission mode),
chat newsays which one and how to answer it, instead of waiting until it times out. - The chat's transcript file appears with the first message, not at launch, so a new chat with no transcript is normal.
- Claude Code names its process after its version, so tmux reports the running command as something like
2.1.286. claude-ops counts that as Claude Code.
Wake a chat when something new happens
This is for a job that should bring a chat in to look at something (new issues on a repo, a particular email, a failed build) instead of sending you a notification. A scheduler (launchd, cron or systemd) runs a small script every few minutes, and the script sends one prompt into the chat only when there is something new.
from pathlib import Path
from claude_ops.watch import State, lock, run_source
with lock(Path("/tmp/repo-watch.lock")) as got:
if got:
state = State(Path.home() / ".local/state/repo-watch/state.json")
run_source(state, "issues", fetch=list_issue_ids,
describe=lambda new: f"[repo-watch] {len(new)} new issue(s). Please read them and tell me what you think.",
target="my-chat")
run_source handles the parts that went wrong for me in earlier versions.
- The first run records what is already there and sends nothing.
- Items are marked as seen only after the send succeeded. If the chat is closed, the same items are offered again on the next run.
- A source that fails several runs in a row (an expired token, for example) sends one alert into the chat instead of going quiet.
- The lock stops two runs from sending the same thing.
The chat-watcher skill explains the full pattern to Claude, including the launchd file.
Skills
claude-ops install skills # copies them to ~/.claude/skills
| Skill | What it is for |
|---|---|
| chat-watcher | building a scheduled watcher that wakes one chat, as above |
| mentor | one chat gives another a task with the standard it must meet, then checks the result itself |
| rover | running something that cuts the Mac off its own wifi, then bringing the wifi back |
Hooks
claude-ops install hooks # prints the settings to add
claude-ops install hooks --write # adds them to ~/.claude/settings.json and keeps a backup
ask-what-you-need (Stop hook). I ask my chats to end long reports with a section called "What I need from you". Questions written there as prose were easy to miss, and then they came back in the next report. This hook stops the turn from ending when that section asks for something and Claude did not use AskUserQuestion. The heading can be changed with CLAUDE_OPS_ASK_HEADING. It lets the turn end on any unexpected input, and it never blocks the same stop twice.
context-check and context-mark (PostToolUse and PostCompact hooks). In a long chat, the summary Claude Code writes when the context is full can lose details. These hooks count the tokens written since the last compaction with Anthropic's count_tokens API, and at 85% of the context they tell Claude to write its own structured summary first and then run /compact. They need an Anthropic API key in ~/.claude/anthropic-api-key (or the file in CLAUDE_OPS_API_KEY_FILE). A small chat never calls the API. The limit, the threshold and the /compact prompt are settings, listed at the top of hooks/context_monitor.py.
Playwright profile slots
If you run several Playwright MCP servers, each with its own browser profile, two chats can try to use the same profile and get "Browser is already in use".
claude-ops slots # free, in use (with the browser's pid) or stale
claude-ops slots --first-free # prints the first free profile name
claude-ops slots --release-stale # removes locks left by a browser that crashed
Chrome marks a profile in use with a SingletonLock link whose target ends with the pid of the browser, so the lock itself says whether the profile is taken. The target of that link is a name and not a file, so os.path.exists says False even for a held lock. claude-ops reads the link instead.
Restore chats that disappeared
Claude Code removes old transcripts over time. If another tool keeps copies (the episodic-memory plugin keeps every chat in ~/.config/superpowers/conversation-archive), this copies back the ones that are missing.
claude-ops restore --dry-run
claude-ops restore
It never overwrites a chat that exists, it keeps each file's original time, and it refuses when more than 200 chats are missing at once (--max), because that usually means a wrong path and not lost chats. Set CLAUDE_OPS_ARCHIVE for another archive folder.
Rover
claude-ops rover path # where the scripts are
claude-ops rover launch mission.sh # runs the mission detached, then brings the wifi back
Some jobs cut the network the chat reaches the Mac over (joining a device's setup network, changing router settings). The harness runs the mission script with a time limit, then power-cycles the wifi and joins your home network again, and a timer forces that return even if the mission hangs. A separate root watchdog (netwatchdog.sh, run by launchd every minute) brings the wifi back if the harness itself dies. The rover skill has the install steps and the rules we learned.
Limits
- iTerm2 sending, the "maybe running" check and the rover scripts are macOS only.
- The check for "is this Claude Code" reads the name of the program in front (node, claude, or a version number). Another node program would also pass, which is why the log exists.
- The inject log is a record, not a security boundary. Any program running as your user can type into your terminals without using claude-ops.
Development
python -m venv .venv && .venv/bin/pip install -e '.[test]'
.venv/bin/pytest
The tests use temporary chat folders, fake senders and fake lock links, so they need no running chats.
License
MIT
Metadata
Release files for claude-ops 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| claude_ops-0.1.0.tar.gz | 43.8 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| claude_ops-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 84.4 kB
Release files / claude_ops-0.1.0.tar.gz
| Download URL | claude_ops-0.1.0.tar.gz |
|---|---|
| Size | 43.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
8735b68d51ada20e84cdcea8118346e25cfcbc34c142ef42c4e10538c9d8f8f8
|
|
BLAKE2b-256 checksum How to use checksums |
46a418ed98db19706810e84e74f0db450dbc94877ead0a2c4f149d4c29bf6a7b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 2, 2026.
Transparency logRelease files / claude_ops-0.1.0-py3-none-any.whl
| Download URL | claude_ops-0.1.0-py3-none-any.whl |
|---|---|
| Size | 40.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
82a0420a412eb506c3756dbfeb8b63948afc65714a7329babe700f35eacd8866
|
|
BLAKE2b-256 checksum How to use checksums |
d9dd51500d09d81b5047f36e63a168831f2cfb9f16517bf4c6c109d97354356b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 2, 2026.
Transparency log