# Handful > Handful is a free, open-source app that runs AI agents, called bots, on your own Mac or Linux computer. It is multi-agent and self-hosted: each bot keeps its own chat, memory, routines and files, and in a team a lead splits a task into steps, the members do them, a second member can check each step, and you approve the result. Every turn runs Claude Code on your machine, on your own subscription, with no Handful account and no Handful cloud. ## Docs The same pages are on the web at https://handful.dev/docs/. This file has their text. - [First steps](https://handful.dev/docs/first-steps/): Install Handful, start Builder, and see what a bot keeps, how rooms and teams work, and what to know about security. - [Bots and rooms](https://handful.dev/docs/bots/): Builder, chats, approvals, memory, routines, skills, artifacts and rooms. - [Teams](https://handful.dev/docs/teams/): How a team takes a task: the lead decides, members do the steps, another member checks, and you approve. - [macOS app](https://handful.dev/docs/mac-app/): The Mac app runs the server from login, keeps itself up to date, and can go back to an older version. - [Configuration](https://handful.dev/docs/configuration/): Flags, config.json, the data folder, model and effort, Handful's key, global instructions and phone access. - [Connectors](https://handful.dev/docs/connectors/): Give bots the tools of an external MCP server, such as Slack or a wiki. - [Development](https://handful.dev/docs/development/): How to build, check and release Handful, and how it works inside. ## First steps **A handful of bots that stay.** Handful is an open-source app that runs personal AI agents, called bots, on your own Mac or Linux computer. Each bot keeps its own chat, memory, routines and files from one conversation to the next. Put a few bots in a team, and a lead decides what they do next. Handful runs [Claude Code](https://code.claude.com) on your own subscription. There is no Handful account and no Handful cloud: the server, the database and the bots all run on your machine. Not affiliated with or endorsed by Anthropic. ### Install and start You need macOS or Linux, and the Claude Code CLI, version 2.1.319 or later. Sign in to it, and make sure that `claude` is on your `PATH`. #### Mac app 1. Download `Handful-universal-mac.zip` from the [latest release](https://github.com/yarlson/handful/releases/latest), unzip it, and move `Handful.app` to `/Applications`. 2. Open the app. If macOS asks, turn on Handful in System Settings › General › Login Items & Extensions. The app runs on Apple silicon and Intel Macs. It keeps the server running in the background from login on, and it has an icon in the menu bar. It downloads a new version by itself, and installs it only when you say so. [More about the Mac app](https://handful.dev/docs/mac-app/). #### From source This works on macOS and Linux. You also need [mise](https://mise.jdx.dev), which installs the Go and Node.js versions that build Handful. ```sh git clone https://github.com/yarlson/handful.git cd handful mise install make build ./bin/handful ``` Then open . ### First steps 1. **Start with Builder.** On the first start, Handful has one bot, Builder. Tell it what you want help with, or pick one of its starts: a personal assistant, a software team, a research desk or a writing studio. Builder asks a few questions, then shows the setup on a card. **Create** makes all of its bots, teams and rooms at once. 2. **Talk to a bot.** Write a message and send it. While the bot works, the chat shows each tool that it uses. Then the bot answers. You can send images and videos, and so can the bot. 3. **Answer when a bot asks.** A bot reads files and searches the web without asking. Before it runs a command, changes a file or calls a connector tool, it shows an approval card: **Allow once**, **Always allow** for that tool, or **Deny**. To make a bot by hand, click **+** next to **Bots**. Give it a name and instructions, for example "Research assistant. Keep answers short." ### What a bot keeps - **Memory.** Bots remember facts across conversations. They save facts on their own, and you can add yours under **Memory** in the bot panel. A fact with **Always in mind** is in front of the bot on every turn. The other facts go to a journal, which the bot searches when it needs to. - **Routines.** Ask for anything that repeats, for example "every weekday at 8:10, brief me on my calendar". The bot saves the prompt and the schedule, in your local time. Pause, run or delete a routine in the **Routines** tab. - **Skills.** A skill is a saved procedure that every bot can use. When you finish a process that you will repeat, a bot can save it as a skill. Run it with `/ `. - **Artifacts.** Bots save documents for each other: plans, tasks, issues, reviews, reports and notes. All of your bots can read them. - **A folder.** Each bot works in its own folder, `~/.handful/bots/`. [More about bots](https://handful.dev/docs/bots/): Builder's limits, file sizes, chat commands, and who can change what. ### Rooms A room is a group chat with you and several bots. Your message wakes the bots that it @mentions, or every member when it mentions no one. The members answer one at a time, so each one sees the earlier replies. A bot's message wakes only the bots that it @mentions. That is enough to hand work around. In this room, four bots ship a change, and each one @mentions the next: ### Teams A room is for conversation. A team is for work. It has a lead, a shared folder, a memory of its own, and a working agreement with your rules, for example "Tests pass, and another member checks each change." Send the team a task in its chat: ``` /task Add CSV export ``` 1. **The lead decides.** The task lands in the team's inbox. The lead takes it, asks a question, puts it off, passes it to another team, or pushes it back with a reason. 2. **The members do the steps.** The lead splits the task into steps. Each step has an owner and acceptance criteria. Steps that do not depend on each other run at the same time. 3. **Another member checks.** When your working agreement asks for a check, a second member reviews each step before it is done. 4. **You approve.** When the steps are done, you approve the result or ask for changes. You do not have to watch a team: - A lead has 30 minutes to decide on a task. After that, the task comes to you. - A team can have a budget of turns per day. At the limit, it pauses until midnight or until you raise the limit. - **Across teams** has one list, **Needs you**: results to approve, decisions that a lead missed, and budgets that ran out. Teams can also send tasks to each other. [More about teams](https://handful.dev/docs/teams/). ### Make it yours - **Rules for every bot.** Put them in `~/.handful/AGENTS.md`. A bot's own instructions win over this file. - **Connectors.** A connector gives bots the tools of an external MCP server, such as Slack or a wiki. See [Connectors](https://handful.dev/docs/connectors/). - **Model and effort.** Set them for one bot, for a team's members, or for every bot. - **Settings.** Click the gear at the bottom of the sidebar. [Configuration](https://handful.dev/docs/configuration/) has the flags, `config.json`, the data folder, the rules for model and effort, and phone access. ### Good to know - You install Claude Code and sign in to it yourself. Handful starts it on your machine for each turn. - Your own Claude Code settings, hooks, `CLAUDE.md` files and MCP servers do not reach the bots. Use `AGENTS.md` and connectors instead. - When a turn reaches your Claude usage limit, Handful starts no new turns until the limit resets. A banner shows when work resumes. Turns that already run go on, and the turn that hit the limit runs again after the reset. - After a restart, turns that were running are marked interrupted, open approval cards expire, and background tasks are gone. - Handful keeps all its data in `~/.handful`. - The UI follows the light or dark mode of your system. ### Security Handful has no login. It is safe only on a computer that one person uses, and only because of these limits: - It listens on loopback only. - It rejects requests whose `Host` is not loopback, to block DNS rebinding. - It rejects requests from another web origin, to block cross-site request forgery. - It rejects API POST requests whose body is not JSON. - Every change through the API needs Handful's key, from the file `~/.handful/ui-key`. Without it, the request gets 401. Handful.app gives its window the key itself. A browser on the computer asks for it once: run `cat ~/.handful/ui-key` and paste the key. A script sends it as the `X-Handful-Key` header. Reads need no key. [Handful's key](https://handful.dev/docs/configuration/#handfuls-key) says how to rotate it. The key stops only code that doesn't know about it, such as a plain request that a prompt slipped into a bot's command. Bots run as your user, so a bot can read the key file, and so can any script that an approved command runs, without showing on any card. An approved command can also change Handful's database directly. Bots run as your user. An approved command can do anything that you can do, so read each approval card before you allow it. Bots are not isolated from each other. Reading files needs no approval and has no path limit. Claude Code's file tools (Read, Grep, Glob) skip Handful's own secrets in the data folder: `ui-key`, `connectors.json`, the tokens in `oauth/`, the per-turn tokens of running bots in `run/`, and the database with its copies (`*.db*` files and `backup/`). A bot can still read them, and any other file, with an approved Bash command, or through a program that such a command runs. A team keeps its members' work focused on the team, but it is not a security boundary: a member can still read any file, including other teams' folders. Phone access is off by default. Turned on in **Settings › Phones**, it opens a second loopback address for a proxy on this computer, and every request needs a paired phone. A phone is paired for one of three scopes: **View**, **Chat**, or **Chat and approve once**. A phone never chooses Always allow. A phone paired for Chat can message your bots, and a bot can then use any tool you have already always-allowed for it, with no approval card. With a Cloudflare Tunnel, Cloudflare sees your chats and approvals. [Phone access](https://handful.dev/docs/configuration/#phone-access) has the keys and the limits. ### Not built yet Routines that start on events (only schedules work now), per-bot Claude Code plugins, a vault for secrets, voice, and bots that run on other machines. ### Development `make build` builds the UI and embeds it in `bin/handful`. `make test` runs the tests, and `make check` runs every check of CI. [Development](https://handful.dev/docs/development/) explains the UI, the Mac app, the website, releases, the checks, and how a turn runs. ### License [MIT](https://github.com/yarlson/handful/blob/main/LICENSE) ## Bots and rooms The details behind the [README](https://handful.dev/docs/first-steps/): Builder, chats, approvals, memory, routines, skills, artifacts and rooms. Teams have [their own page](https://handful.dev/docs/teams/). ### Builder On the first start, Handful has one bot, **Builder**. It sets up bots, teams and rooms for you. 1. Tell Builder what you want help with, or pick a start from its first message: a personal assistant, a software team, a research desk or a writing studio. 2. Builder asks a few questions and proposes a setup in the chat. 3. When you agree, Builder shows the setup on a card: each new bot with its instructions, and each team or room with its members. **Create** creates all of it at once. **Cancel** creates nothing. The card has no **Always allow**, so Builder asks you for every setup. - A setup adds at most 8 bots, 3 teams and 3 rooms. A new name must differ from the names of your bots, teams and rooms. - The new bots do not introduce themselves. Builder tells you where to start, for example to open a team and send it a task. - The card shows everything that the setup creates. A setup whose texts hold an invisible character, or two blank lines in a row, is refused. - Builder cannot change or delete your other bots, teams or rooms, and it joins no team or room. Do that in the app. - Handful creates Builder only when it starts with no bots, teams or rooms. After you delete Builder, it does not come back while you have other bots. ### Create a bot 1. Click **+** next to **Bots**. 2. Give the bot a name and instructions, for example "Research assistant. Keep answers short." 3. The bot starts and introduces itself in its chat. Each bot works in its own folder, `~/.handful/bots/`. The bot id is the last part of the chat address, `/chat/`. ### Talk to a bot - Write a message and send it. While the bot works, the chat shows each tool that it uses. Then the bot answers. - To send images or videos, paste them into the message box, drop them on the chat, or click the paperclip. Bots can send you images and videos too. - A message can carry up to 4 files, 100 MB in total. - Images: PNG, JPEG, GIF or WebP, up to 10 MB each. - Videos: MP4, WebM or QuickTime, up to 50 MB each. - To react to a message, yours or a bot's, hover over it, click the smiley, and choose 👍, 👎, ❤️, 😂, 👀 or ✅. Click a reaction to remove it. Reactions are for you only: bots do not see them, and a reaction never wakes a bot. - A bot can start a long command in the background and keep talking to you. The chat lists the running tasks above the message box. **Stop** ends them. - Type `/` at the start of a message for commands: - `/clear` starts a fresh conversation context. The chat stays visible. - `/compact` shortens the context and shows the token counts before and after. A bot also compacts its context on its own before it reaches 200k tokens, at about 160k. - `/ ` runs a skill now (see [Skills](https://handful.dev/docs/bots/#skills)). - The bot panel's **Settings** shows how much context the bot carries and how its last turn went, for example "Context: 142k of 200k · last turn: 3 calls, from cache". "Rebuilt the cache" means that the turn wrote the bot's context to the cache again, which costs more, as after a long pause, and always on the first turn of a fresh context. ### Approve what a bot does A bot reads files and searches the web without asking. Before it runs a command, changes a file, or calls a connector tool, it asks you with an approval card: - **Allow once** lets this one call run. - **Always allow <tool>** lets this bot use the tool without asking. To remove this permission, open the bot panel, then **Settings**, then **Always allowed tools**. - **Deny** stops the call. A card that you do not answer expires after 4½ minutes, or when the bot stops working. A setup card from Builder has **Create** and **Cancel** instead (see [Builder](https://handful.dev/docs/bots/#builder)). In the sidebar, a shield marks a bot that waits for an approval. A question bubble marks a bot that asked you something. ### Memory Bots remember facts across conversations. To see and edit them, open the bot panel with the button at the top right, then **Memory**. - When you add a fact with **Always in mind**, the bot sees it on every turn. Without it, the fact goes to the bot's journal. The bot searches the journal when it needs to. - **Shared with all bots** gives the fact to every bot, not only this one. - A team has memory of its own too: see [Teams](https://handful.dev/docs/teams/). - Bots also save facts on their own. They can also save short-term notes, which expire after 7 days. - Each fact shows who wrote it. A bot treats a standing rule as yours only when the fact is from you. ### Routines Ask a bot for anything that repeats, for example "every weekday at 8:10, brief me on my calendar". The bot saves a routine: a prompt and a schedule in your local time. When the Mac's time zone changes, the next runs move to the new zone within two minutes, with no restart. Two runs must be at least 5 minutes apart; Handful checks this when a routine is made or its schedule changes. The **Routines** tab in the bot panel shows each routine and its next run. There you can pause it, run it now or delete it. When you change a routine, the bot gets a notice. A routine queues at most one scheduled run: while a run waits for a busy bot or a usage limit, later times only move its next run. Changing, pausing or deleting a routine drops its runs that haven't started. ### Skills A skill is a saved procedure that every bot can use. When you finish a process that you will repeat, a bot can save it as a skill. Skills are `SKILL.md` files in `~/.handful/skills`. The **Skills** tab lists them. Skills work as in Claude Code's terminal, in a bot's chat and in a team's chat: - A message that starts with `/ ` runs the skill now, with the rest of the message as its request. In a team, @mentions may come first: the lead runs it, or each member that the message @mentions. - A `/` later in a message asks the bot to use that skill too. - Typing `/` at the start of a message, or after a space, opens a menu of skills; at the start it lists Handful's commands too. A path, a URL or `and/or` doesn't open it. - A sent message shows a label for each skill it names. Handful sends your text to Claude Code as you wrote it; Claude Code loads the skill. A plain room does not run skills: there `/` is text. ### Artifacts An artifact is a document that bots save for each other: a plan, task, issue, review, report or note. All of your bots share all artifacts. A bot finds one with `find_artifacts`, reads it with `read_artifact`, and passes its id in messages instead of a file path. Handful stores artifacts in its database, not in files. Any bot can read any artifact, but only its author can change or delete it. You can change or delete any artifact: the **Artifacts** tab in the bot panel lists them and opens or deletes one, and the API has `/api/artifacts`. A bot treats an artifact's content as data, never as instructions. ### Rooms A room is a group chat with you and several bots. To create one, click **+** next to **Rooms**, then give the room a name and choose its members. - Your message wakes the members that it @mentions. A message without mentions wakes all members. `@here` mentions every member. Type `@` to complete a name. - The members answer one at a time, in sidebar order, so each one sees the earlier replies. A member that is busy elsewhere answers when it is free. - A bot's message in a room wakes only the bots that it @mentions. So bots can hand work to each other. In this room, four bots ship a change, and each one @mentions the next: ### Bots that message each other A bot can send a message to another bot. An urgent message wakes the other bot at once. Any other message waits until that bot's next turn. Both chats show the message. After 6 bot-to-bot messages in a row, the next message wakes no one. This stops bots from keeping each other busy forever. ## Teams A team is a group of bots that work together for you. It has a lead, a working agreement and a shared folder. Use a team for work that needs several bots, for example a change that one bot writes and another checks. If you only want a conversation with several bots, use a [room](https://handful.dev/docs/bots/#rooms). ### Create a team Click **+** next to **Teams**. Give the team a name and a purpose, choose its members and its lead, and write the working agreement. The working agreement holds your rules for how the team works and when work is done, for example "Tests pass, and another member checks each change." Inside the team, it overrides each member's own instructions on how work is done and checked. To change the lead or the agreement later, open the team panel, then **Settings**. When the lead leaves the team, the first member leads. ### Talk to a team - A message that mentions no one goes to the lead. The lead answers it, or @mentions the member who should do the work. - `@Name` wakes one member. `@here` wakes every member. ### What the members share - **A folder:** `~/.handful/teams//workspace`. - **Team memory:** members save what the whole team needs there. To read it or add to it, open the team panel, then **Memory**. Members see team memory only in the team's turns. A bot can be a member of several teams. It keeps a separate Claude Code session for each one. In a team turn, a member talks only to its team. It does not message other bots, and it does not see the direct messages that other bots sent it. Those wait for its next turn outside the team. There is one exception. A member can post to a room that it belongs to, as long as the post @mentions no one, so that no bot wakes. A lead's report is an example. Handful refuses a post that would wake someone, and names the bots it would wake. ### Tasks For work with several steps, send the team a task. Type `/task` and what the team should do in the team's chat: ``` /task Add CSV export ``` `/goal` still works. **New task…** in **Across teams** does the same for any team. Its **Start now** box puts the task at the top of the team's plan. You can also ask a team's lead in its own chat, for example "add CSV export to the team's tasks". The lead adds it to the team's board as your task, the same as `/task`, and starts it at once only when you ask for that. Only the lead does this, in its own chat, in a turn that your message started: a member, a routine, or a turn that another bot's message started cannot add a task this way. Messages that other bots left for the lead arrive in that turn too, so check that the task is one you asked for. #### 1. The lead decides Every task lands in the team's inbox first, whether it comes from you, from another team or from a member. The lead decides on each one. It can: - take the task, at a place in the plan; - ask the sender a question; - defer it to a date; - pass it on to another team; - push it back, with a reason. The lead has the team's **decision time** for each task: 30 minutes by default. The clock counts only the time when the lead can act. The inbox shows the clock as text, for example "decide by 14:30" or "clock paused: usage limit". The task comes to you when the time runs out, or when the leads have asked about it or passed it on 3 times. Then you **Take** it at a place in the plan, or **Push back…** with a reason. #### 2. The members do the steps The task that the team works on is the **Now** task. The lead splits it into steps. Each step has a brief, acceptance criteria and one owner. When the working agreement asks for a check, the step also has a reviewer, who is another member. - A step starts when it is ready. A step that depends on others waits for them. Independent steps run at the same time. - The owner submits the result. The reviewer passes it or asks for changes. Every third request for changes blocks the step, and the lead decides what to do. - An owner or a reviewer that cannot go on blocks the step with a question. The lead answers, and the step goes back to its owner or reviewer, or to another member. The lead can also ask you in the chat. While a bot reviews a step, Handful denies it Edit and Write in the team, even when you always allow them. Bash can still change files, so do not always allow Bash for a bot that reviews. Two things can leave a step without anyone on it: - **A member stops without handing its step on**, for example after a failed turn or a restart. The step is blocked, and the lead decides. If the task then has nothing in flight, the lead wakes. - **A member leaves the team, or you delete it.** Its steps wait for a new owner. A review that it did not finish is blocked for the lead. #### 3. Whoever asked approves When every step is done or dropped, the lead closes the task with a result, and the next task in the plan starts at once. The done task waits for approval by whoever asked for it: you for your tasks, the sending team's lead for its tasks. **Approve** it, or **Ask for changes…** With changes, the task comes back as Now, first in the plan. After you approve a task, the lead looks back on it once. It is asked to save up to 5 lessons in team memory. It can also suggest a change to the working agreement, but only you change the agreement. #### Stop or pause a task - **Stop…** on the Now task, in the **Work** tab, stops the task. Running turns finish, its steps stay, and the next task in the plan starts. The stopped task moves to **Earlier**. **Reopen** there puts it back in the inbox. - **Stop** in the team's chat pauses the task. The Now card then shows **Paused** and **Resume**, which goes on with it. ### Where you see the work #### The Work tab The **Work** tab of the team panel shows, from top to bottom: | Section | What it shows | |---|---| | **Now** | The task in work: its steps, how many are done, the turns it used, when it started, **History** and **Stop…**. | | **Needs you** | What only you can settle in this team. It shows only when there is something. | | **Next** | The planned tasks in the lead's order, and, when the inbox has tasks, a line that opens it. It shows only when there is something. Point at a planned task, or tab to it, to move it with the arrows, pin it so the lead can't move it, or **Withdraw…** it. | | **Waiting on other teams** | The open tasks that this team sent. With more than one team, **Send a task…** is here. | | **Earlier** | Stopped tasks, with **Reopen**, sent tasks that are over, and pushed-back tasks you dismissed. It is closed until you open it. | | The bottom | A note on turns that were on no task, when there are any, then **Today**, with the day's turns. | When the panel is closed, a **Now** chip in the chat header shows the Now task and how many things need you. Click it to open the Work tab. #### Across teams **Across teams**, at the top of the sidebar, shows every team at once: its Now task, how many tasks are next and in its inbox, its turns on no task, today's turns, and whether it is paused. Its **Needs you** list holds what only you settle: - results to approve; - decisions that a lead did not make in time; - tasks that were asked about or passed on 3 times; - budgets at 80% or used up, with **Raise…**, and **Wait until midnight** once used up; - open questions. ### The budget A team has one budget of turns per day. - Every turn of its members counts, chat and @mentions included. The lead's decisions on the inbox do not count. - The team that does a task pays for it. - Set **Turns per day** in the team's **Settings**. 0 is no limit, and a new team starts with no limit. - The Work tab shows the day's count under **Today**. After a week it suggests a limit, and **Use** sets it. - At 80%, **Needs you** warns you. At the limit, the team pauses until midnight or until you raise the limit. Running turns finish, no new turn starts, and the lead's decisions still run. - Each task shows its turns so far. **Turns on no task.** The Work tab notes the team's turns of the last hour that were on no task: who, when and the first line. **Make a task…** turns them into a task that the team takes at once. **Dismiss** puts the note away. The lead sees the same count and can make the task too. With `-max-parallel` above 1, a team's task turns leave one slot free, so your other bots still answer while a team works. ### Teams that work together Teams pass work to each other as tasks. A task can be a whole piece of work, for example a bug for a Dev team or an approved spec to build. It can also be a part that a step of the sending team's Now task waits on, for example a guide that a Release team needs from a Docs team. The receiving team works on the task from its own budget. The sending team needs no task of its own. - **Send.** A lead sends a task with a brief and any artifacts. You can too: **Send a task…** in a team's **Work** tab, or **Send to a team…** on an artifact in the **Artifacts** tab. - **Receive.** The task lands in the receiving team's inbox, and the sidebar shows a blue count on the team. Its lead decides on it, as on any task. - **Follow.** The sending team's **Work** tab lists the tasks it sent in the last 30 days. The open ones are under **Waiting on other teams**, each with its state: in the other team's inbox, later, its place in the other team's plan, or being done, with its turns ("paid by" the receiving team). A task the receiving lead asked about, finished, or pushed back moves to **Needs you**. The ones that are over (done, stopped or withdrawn) are under **Earlier**. - **Act.** In **Needs you**: **Answer…** replies to the receiver's question, **Approve** or **Ask for changes…** settles a task done at the other team, and **Dismiss** puts away a task that was pushed back, after you read the reason; it then moves to **Earlier**. Under **Waiting on other teams**, **Withdraw…** takes back a task that hasn't started. When a step of the sender's task waits on the sent task, the receiving lead sees "'s task waits on this". If the task is pushed back, that step is blocked with the reason, and the sending lead plans again. Limits: - A team's inbox holds at most 10 tasks, and at most 3 from one team. Change both, and the decision time, in the team's **Settings**. - Your own tasks always land. - Work that came through 3 teams needs you to send it on. ## macOS app The Mac app keeps the Handful server running in the background, from login on, with an icon in the menu bar. It runs on Apple silicon and Intel Macs. 1. Download `Handful-universal-mac.zip` from the [latest release](https://github.com/yarlson/handful/releases/latest), unzip it, and move `Handful.app` to `/Applications`. 2. Open the app. If macOS asks, turn on Handful in System Settings › General › Login Items & Extensions. To build the app yourself, run `make app` and move `desktop/dist/mac-arm64/Handful.app` (`desktop/dist/mac/Handful.app` on an Intel Mac) to `/Applications`. A copy that you build has an ad-hoc signature; macOS opens it because it was not downloaded. - The server keeps running when you quit the app, and starts at every login. It has no login of its own, so use the app only on a Mac that one person uses: see [Security](https://handful.dev/docs/first-steps/#security). - The icon in the menu bar has two items. **Check for updates…** opens the app and checks for a new version. **Exit server** stops the server, which then stays stopped until you open the app again or log in again. - **Handful › Settings…** (⌘,) opens the Settings page. A change that needs a restart says so. **Restart server** restarts the server at once, or, if you choose, waits for the running bots first. - **Handful › Open Settings File** opens `~/.handful/config.json` in your default editor. - When the server can't start, the error screen shows its last error and offers **Open config.json**. - Links in chats open in your default browser. - The server writes its log to `~/Library/Logs/Handful/server.log`. - To remove the app, turn off Handful in System Settings › General › Login Items & Extensions, then delete `Handful.app`. Your bots and chats stay in `~/.handful`. The app also leaves `~/Library/Logs/Handful` and `~/Library/Application Support/Handful`; delete them too to remove everything. ### Updates The app keeps itself up to date. - Once a day it checks the latest release on GitHub, and downloads a new version in the background. A pill in the window then shows it. **Later** hides the pill until the next launch or for a day; **Skip this version** hides it until a newer version comes out. - It installs an update only when you ask: **Restart to update** in the pill or in the **Handful** menu, or ⌘Q. Then the app and its server restart, in about half a minute. - A restart of the server stops the turns that run. So when bots work, the app asks first. **Update when they finish** waits for their running turns, then restarts. After 30 minutes it asks you again, to restart now or to wait longer; it never restarts on its own while bots work. - **Handful › Check for updates…** and **Check for updates…** in the menu-bar icon check at once. - To turn off the daily check, put `"updates": {"autoCheck": false}` in `~/.handful/config.json`. **Check for updates…** still works. `drainMinutes`, from 5 to 240, sets how long the app waits for running turns, for example `"updates": {"drainMinutes": 60}`. The app reads these settings at each check, so they need no restart. - A copy that you build yourself has an ad-hoc signature, so it can't update itself: macOS installs an update only when its signature matches the running app's. It only shows that a new release is out. #### Go back to an older version 1. Put `"updates": {"autoCheck": false}` in `~/.handful/config.json`, so that the app doesn't update itself again. A version without updates doesn't know this setting and doesn't start with it; to go back to such a version, remove `updates` instead. 2. Download `Handful-universal-mac.zip` of the version you want from [Releases](https://github.com/yarlson/handful/releases), and unzip it. 3. Quit Handful with ⌘Q, and replace `/Applications/Handful.app` with the older app. 4. Open it. When bots work, it waits for their running turns before it starts its own server. ## Configuration | Flag | Default | Meaning | |---|---|---| | `-addr` | `127.0.0.1:7777` | Listen address. Handful refuses an address that is not loopback. | | `-data` | `~/.handful` | Data folder. Without it, the first start moves an older data folder here. | | `-claude` | `claude` | Claude Code CLI to run | | `-model` | Claude Code's default | Model for every bot without its own: an alias (`fable`, `opus`, `sonnet`, `haiku`) or a full model ID | | `-effort` | the model's own | Reasoning effort for every bot without its own: `low`, `medium`, `high`, `xhigh` or `max` | | `-max-parallel` | `3` | Turns that can run at the same time, across all bots | | `-login-env` | off | Take the environment, such as `PATH`, from your login shell. The macOS app uses it, because launchd starts the server with a minimal `PATH`. | | `-tray` | off | Show an icon in the macOS menu bar with two items: **Check for updates…**, which opens the app and checks for a new version, and **Exit server**, which stops the server. The macOS app uses it. | | `-log` | none | Append the log and all other error output to this file. `~/` is your home folder. Without it, they go to stderr. | | `-move-back` | off | Go back to the data folder as it was before the move: say what would be lost, then stop. | | `-yes` | off | With `-move-back`, go back. | To set these without flags, put `claude`, `model`, `effort` or `maxParallel` in `~/.handful/config.json`, for example `{"model": "sonnet", "effort": "high", "maxParallel": 4}`. - A flag wins over the file. - Handful reads the file only when it starts, so a change needs a restart. - If the file is invalid, Handful does not start, and the error names the file. The data folder holds: | Path | Content | |---|---| | The SQLite database | Bots, chats, memory, routines, artifacts | | `bots//` | Each bot's working folder | | `teams//` | Each team's shared folder | | `skills/` | Skills that all bots share | | `media/` | Images and videos of messages | | `AGENTS.md`, `connectors.json`, `config.json` | Your optional settings | | `ui-key` | Handful's key, which every change through the API needs (mode 0600) | | `oauth/` | Connector sign-in tokens | | `run/` | MCP configs of running bots, with their per-turn tokens | ### Model and effort Each bot can have its own model and reasoning effort: set them in the bot's **Settings**, in the **New bot** dialog, or ask Builder. A team can give its members a default model and effort in its **Settings**, under **Member defaults**. Model and effort are resolved separately, in this order: 1. the bot's own value; 2. in a team's chat, the team's default; 3. the global value: the `-model` or `-effort` flag, then `config.json`; 4. otherwise Handful passes nothing, and Claude Code uses its own default. - **Where you see them.** The chat header shows a bot's model and effort, and a team's defaults. A team's or room's **Members** tab shows each member's values and where they come from. Hover or focus a bot's name on a post to see the model and effort that wrote it. Effort is shown as *requested*, because Claude Code doesn't report the level it applied; a model that doesn't support a level runs at the nearest lower one. - **When a change applies.** From the bot's next turn. Switching models makes that turn slower and costlier, because the prompt cache starts over. - **Problems.** When Claude Code can't run a model, the chat says so, names where the value comes from and links to Settings, and the turn isn't retried. When Claude Code runs another model than the one asked for, the header chip turns amber and the chat notes it once. A bot can't change its own model or effort. - **Environment.** When Handful sets an effort, it removes `CLAUDE_CODE_EFFORT_LEVEL` from the bot's run, since that variable would outrank it; with no effort in Handful, the variable applies. `ANTHROPIC_MODEL` stays, because `--model` outranks it. Handful always removes `CLAUDE_CODE_SUBAGENT_MODEL`, `CLAUDE_CODE_SUBAGENT_EFFORT` and `CLAUDE_CODE_SUBAGENT_MODEL_FORCE`, so a bot's helper subagents use the bot's model and effort. ### Handful's key Every change through Handful's API (any request under `/api/` other than GET) needs Handful's key; without it, the request gets 401 with `{"error": "This needs Handful's key."}`. Reads need no key, and neither does a paired phone, which has its own session. - **Where it is:** the file `ui-key` in the data folder, so `~/.handful/ui-key`, or `/ui-key` with `-data `. Handful makes it at its first start, readable by you only (mode 0600), and keeps it across restarts. A file that others can read is narrowed to 0600 at the next start. - **Handful.app** reads the file and gives its window the key, so you never see it. - **A browser on the computer** shows a page that asks for the key once: run `cat ~/.handful/ui-key` in a terminal (the page shows the path in use), and paste the key. The browser keeps it in a cookie. - **A script** sends it as the `X-Handful-Key` header, for example `curl -H "X-Handful-Key: $(cat ~/.handful/ui-key)" …`. - **To rotate it,** delete the file and restart Handful. Every browser then asks for the new key. The key stops only code that doesn't know about it, such as a plain request that a prompt slipped into a bot's command. Bots run as your user, so a bot can read the key file, and so can any script that an approved command runs, without showing on any card. An approved command can also change Handful's database directly. Bots are not isolated from each other or from Handful's files. ### Global instructions Put instructions for every bot in `~/.handful/AGENTS.md`, for example: ```markdown - Never add AI attribution to commits or pull requests. - Ask me before any merge. ``` - A bot's own instructions win over this file. - A change reaches a bot when it next starts a Claude Code process. - Every bot can read the file, so keep tokens out of it. - A file over 32 KiB stops every bot's turns until you shorten it. ### Phone access Phone access lets you use Handful from your phone. It is **off by default**, and with it off Handful opens no other port. Turn it on in **Settings › Phones**: pick the way in (a Cloudflare Tunnel or Tailscale), follow its setup text, fill in the phone address (and, for a tunnel, the Access team domain and AUD tag), and choose **Turn on**. The phone address opens at once, without a restart, and the settings are saved in `config.json`. **Turn off phone access** on the same page closes it again. To pair a phone: 1. On the computer, choose **Pair a phone**, and what the phone may do. Settings shows a QR code and the same code as text, such as `K7QM-2XPD-9WTR`. The code works once and ends after 5 minutes. 2. On the phone, open the phone address, scan the QR code or type the code (in any case, with or without dashes), and name the phone. 3. On the computer, choose **Allow** in the request that shows at the top of every screen, within 2 minutes. The phone is paired. After 5 wrong codes in 10 minutes, pairing pauses for 10 minutes. The Phones list on the same page shows each paired phone, with **Revoke**, and **Revoke all**. The settings can also be written in `config.json` by hand; then they apply after a restart: ```json { "phone": { "enabled": true, "hosts": ["phone.example.com"], "proxy": "cloudflare", "cloudflare": {"teamDomain": "you.cloudflareaccess.com", "aud": ""} } } ``` | Key | Default | Meaning | |---|---|---| | `phone.enabled` | `false` | Open the phone address | | `phone.addr` | `127.0.0.1:7778` | The phone address. It must be loopback. | | `phone.hosts` | none | The names your phone opens, 1 to 10. Not `localhost`, and not a quick tunnel (`trycloudflare.com`), which has no login in front. | | `phone.proxy` | `cloudflare` | `cloudflare` for a Cloudflare Tunnel with a Cloudflare Access login in front, or `tailscale` for `tailscale serve` | | `phone.cloudflare.teamDomain` | none | The team domain of the Access login. Needed for `cloudflare`. | | `phone.cloudflare.aud` | none | The AUD tag of the Access application, from its Overview page. Needed for `cloudflare`. | If a phone key is invalid, or phone access is on and a key it needs is missing, Handful does not start, and the error names the key. - **A second listener.** A proxy on this computer forwards your phone's requests to the phone address. Handful still never listens on your network itself. - **Every request needs a paired phone.** Only the web app's own files and pairing work without one. A phone is paired for one of three scopes: **View** (read only), **Chat** (also send messages and act on tasks), or **Chat and approve once** (also Allow once and Deny on approval cards). A phone shows only the controls its scope may use. - **A phone never chooses Always allow**, and never allows a setup that creates bots or teams. Settings, bots, teams, routines, memory, skills, connectors and phone access itself stay on the computer. - **A phone paired for Chat can message your bots.** A bot can then use any tool you have already always-allowed for it, with no approval card. - **With a Cloudflare Tunnel, Cloudflare sees the traffic**: your chats, what bots print and your approvals. Handful also checks the Cloudflare Access login on every request, so the public address answers nothing without it. - **A decision from a phone** shows on the approval card with the phone's name, such as "Approved once from Pixel 8." - **Revoking a phone** makes its next request fail and ends its open streams within 15 seconds. **Turning phone access off** closes the phone address and its streams at once; paired phones stay paired. - **The desktop app** hears of a waiting pair request through the page's `handful:pair-request` event, whose detail is 1 while a request waits and 0 after. ## Connectors A connector gives bots the tools of an external MCP server, such as Slack or a wiki. List the servers in `~/.handful/connectors.json`: ```json { "servers": { "wiki": { "url": "https://wiki.example/mcp", "bots": [""] }, "search": { "url": "https://search.example/mcp", "headers": {"Authorization": "Bearer "}, "bots": [""] } } } ``` 1. Make the file private: `chmod 600 ~/.handful/connectors.json`. The file can hold tokens, so Handful does not read it otherwise. 2. Sign in to each server that has no `headers`: click **Connectors** at the bottom of the sidebar, then **Connect**, and sign in on the page that opens. Handful uses OAuth 2.1, with dynamic client registration and PKCE. Rules: - A server with `headers` gets them on each call. For a server that needs no sign-in, set `"headers": {}`. - A server name uses only `a-z`, `0-9` and `-`. The name `handful` is reserved. A bot calls the server's tools as `mcp____`. - The `url` must use https. Plain http is allowed only for `localhost`, `127.0.0.1` and `::1`. - `bots` lists the bots that load the server. It does not hide the tokens from other bots (see [Security](https://handful.dev/docs/first-steps/#security)). - Connector tools go through approval cards. **Always allow** applies to one tool, not to the whole server. - Handful reads the file each time a bot starts a Claude Code process, so a change needs no restart. An invalid file fails the bot's turn with an error that names the problem. - Handful keeps sign-in tokens in `~/.handful/oauth/.json`, with mode 0600, and refreshes them when they are about to expire. **Disconnect** deletes them. - When a server is not signed in, the bot works without it, and the chat says that the server needs sign-in. - If you change `-addr`, connect again: the sign-in address contains the port. A server without dynamic client registration needs a client that you register yourself: ```json "oauth": {"clientId": "", "clientSecret": "", "scopes": [""], "callbackPort": 3118} ``` All `oauth` fields are optional. With `callbackPort`, the sign-in returns to `http://localhost:/callback`, as in Claude Code's `.mcp.json`. For example, Slack's MCP server works with Slack's public client for Claude Code ([Slack docs](https://docs.slack.dev/ai/slack-mcp-server/connect-to-claude)). A Slack workspace admin must approve it first: ```json "slack": { "url": "https://mcp.slack.com/mcp", "oauth": {"clientId": "1601185624273.8899143856786", "callbackPort": 3118}, "bots": [""] } ``` ## Development How to build, check and release Handful, and how it works inside. ### Build and work on the UI `make build` builds the UI into `ui/dist` and embeds it in `bin/handful`. To work on the UI, start `./bin/handful`, then run `cd ui && npm run dev`. Vite sends `/api` requests to port 7777. `desktop/` holds the macOS app: the Electron window (`main.js`, `app.js`, and `context-menu.js`, which builds the right-click menu and has its own tests), the updater (`updater.js`, `preload.js`, and `updater-state.js` and `updater-io.js`, which have no Electron code and their own tests), the launch agent that runs the server (`build/dev.yarlson.handful.server.plist`), and `manual-move.js`, which stops the old app's server before Handful registers its own, and has its own tests. `make app` builds it for this Mac, and `make app-universal` builds the zip for Apple silicon and Intel Macs. Its server also listens on port 7777, so choose **Exit server** before you start `./bin/handful`. `make icons` regenerates the menu-bar icon from its SVG source and the site's apple-touch-icon; it needs `rsvg-convert` (Homebrew `librsvg`). The app icon, `desktop/build/icon.icns`, is a committed brand asset that no target rebuilds. `mise.toml` pins Go, Node.js and every check tool, and the Makefile runs them through `mise exec`. `make app-universal` and `make icons` also use macOS tools that mise does not install: `lipo` and `sips`, and `rsvg-convert` from Homebrew `librsvg`. Coding agents get their working rules from `AGENTS.md`. ### Website `web/` holds the handful.dev website, an Astro site. To work on it, run `cd web && npm ci && npm run dev`, then open . `npm run build` runs `astro check` and writes the site to `web/dist`. ### Releases Push a tag such as `v0.2.0` to release; the tag must be a version, such as `v0.2.0` or `v0.2.0-rc.1`. `.github/workflows/release.yml` then runs `make app-universal VERSION=0.2.0` on a macOS runner, which signs the app with a Developer ID and the hardened runtime and has Apple notarize it, and uploads the zip as `Handful-universal-mac.zip` to the release of the tag. When no such release exists, it creates one; a tag with a hyphen, such as `v0.2.0-rc.1`, makes a pre-release. When you publish a release on GitHub, which also pushes its tag, the release keeps your title and notes. handful.dev links `releases/latest/download/Handful-universal-mac.zip`, so it needs no change for a new release. The release also gets `handful-mac.yml` and the zip's blockmap, which the app's updater reads, and the bridge files for the apps from before the rename. [docs/releasing.md](https://github.com/yarlson/handful/blob/main/docs/releasing.md) explains the release, the bridge files, staged rollouts, how to stop a bad release, and why the signing certificate must not change. ### Checks ```sh make test # Go tests, the TypeScript type check and the UI unit tests make check # every check of CI make fmt # format Go and UI code ``` The agent tests replace Claude Code with a fake CLI script and run a real MCP client against the tool endpoint. `make check` runs these targets. CI runs them as six jobs at the same time: `go-check` (the targets from `fmt-check` to `vuln` that check Go code), `race`, `ui-check`, `web-check`, `desktop-check` and `repo-check` (`workflow-check` and `name-check`). The `check` job fails when one of them fails. CI keeps Go's build and test cache and the npm cache from the last run on main. Go reports a test result from the cache only when the test binary and every repository file and environment variable that the test read are unchanged, so a change that touches no Go code tests nothing again. The weekly run tests everything again. | Target | Fails when | |---|---| | `fmt-check` | Go code is not formatted with gofumpt and goimports | | `lint` | golangci-lint, configured in `.golangci.yml`, reports an issue | | `generate-check` | The sqlc code in `internal/db` differs from what `sqlc generate` writes | | `mod-check` | `go.mod` or `go.sum` is not tidy, or a module does not match `go.sum` | | `race` | A Go test fails, or the race detector finds a data race | | `vuln` | govulncheck finds a known vulnerability in code that Handful calls | | `ui-check` | The UI is not formatted with oxfmt, oxlint reports an error, the type check or a vitest test fails, or the build fails. oxlint warnings do not fail it. | | `web-check` | `astro check` finds an error in the website, or its build fails | | `desktop-check` | A unit test of the macOS app, its updater or `desktop/scripts/release-feed.js` fails | | `workflow-check` | actionlint finds a problem in a GitHub Actions workflow | | `name-check` | The old name is left in a file outside `scripts/name-allow.txt` | Slower analyses have their own targets. They write their reports to `target/`. | Target | What it does | |---|---| | `mutation-diff` | Mutation-tests, with gremlins, the Go lines changed since `BASE` (default `origin/main`). It fails when a mutant on a changed line survives the tests, or when no test runs that line. Git must track new files, for example with `git add -N`. CI runs it on each pull request that changes such a line. | | `ui-mutation-diff` | The same for the lines of `ui/src/lib` changed since `BASE`, with Stryker. CI runs it on each pull request that changes such a line. A pull request that changes only the website, the docs or other files runs neither. | | `mutation` | Mutation-tests all Go code, in about 8 minutes on 10 cores. It fails below 81 % efficacy or 85 % mutant coverage. CI does not run it. `cmd/handful` is not mutation-tested: gremlins 0.6.0 cannot map its coverage. | | `crap` | Prints the CRAP score of each hand-written Go function: its complexity combined with its test coverage. Higher means riskier to change. | | `ui-mutation` | Mutation-tests `ui/src/lib` with Stryker and prints the score. | ### How it works ``` browser ──REST + SSE──▶ Handful server (Go) ──▶ SQLite │ ▲ spawns per turn│ │MCP over HTTP (bearer token per turn) ▼ │ claude -p … (cwd = the bot's workspace) ``` - **Turns.** A message, a routine or a bot's first start becomes a turn. A bot runs one turn at a time, and its turns share one Claude Code session, which each turn resumes. The prompt goes to the `claude` process on stdin as stream-json. Each send carries a `clientNonce`, so a retried send does not start a second turn. - **Delivery.** The model's plain text is private. The user sees what the bot sends with the `send_to_user` tool. When a turn ends without a message, the chat shows the model's final text, marked as a fallback. - **Tools.** Each `claude` process connects to Handful's MCP endpoint, `/mcp`, with a token that is valid only for its turn. The tools are `send_to_user`, `send_to_agent`, `read_room`, `recall_memory`, `update_memory`, `update_routine`, `update_skill`, `find_artifacts`, `read_artifact`, `update_artifact`, `update_profile`, `update_board`, `create_setup` (Builder only) and `approve`. Tool calls that need approval reach `approve` through `--permission-prompt-tool`. - **Isolation.** Workers run with `--setting-sources project`, `--strict-mcp-config`, `--settings` that turn off auto memory and commit and PR attribution, `CLAUDE_CODE_DISABLE_BUNDLED_SKILLS=1` and `CLAUDE_CODE_DISABLE_CLAUDE_MDS=1`. The built-in tools are limited to Task, Bash, Monitor, TaskStop, Read, Edit, Write, Glob, Grep, WebSearch and Skill. Handful writes each process's MCP config to a 0600 file in `/run`, so no token shows in the process list, and removes the file when the process exits. - **Transcript.** Each chat is an ordered log of `user`, `bot`, `event`, `activity` and `approval` entries. Each change gets a new `rev`. Clients catch up with `GET /api/bots/{id}/entries?after=` and the `GET /api/events` stream. A change can reach the stream before one that took the rev just below it, so the UI catches up from the highest rev with no gap below it, not from the highest rev it has seen. - **Schema.** The migrations in `internal/db/migrations` run in order at start, each in one transaction with its `PRAGMA user_version` update. ### Layout | Path | Content | |---|---| | `cmd/handful` | Server entry point and flags | | `internal/db` | Schema, sqlc queries and generated code (`modernc.org/sqlite`, pure Go) | | `internal/core` | Domain state: bots, teams, transcript, turns, memory, routines, skills, artifacts, approvals, event hub | | `internal/agent` | Claude Code runner, stream-json parsing, system prompt and MCP tools | | `internal/httpapi` | REST API, event stream, origin guard and UI hosting | | `internal/shellenv` | The environment of the user's login shell, for a server that launchd starts | | `ui` | React 19, TypeScript 7, Tailwind v4 and shadcn/ui app, embedded through `ui/embed.go` | | `desktop` | The macOS app: Electron window, launch agent and icons | | `web` | The handful.dev website: Astro 4 and Tailwind 3 | ## Bot tools Each `claude` process connects to Handful's MCP endpoint, `/mcp`, with a bearer token that is valid only for its turn. The bots use these tools. They are not for external clients. - `send_to_user`: send a message to the user. This is the only output the user sees. In a room turn, it posts into the room. - `text`: markdown. It can be empty when files are attached. - `files`: up to 4 paths of images or videos. - `needs_reply`: true when the bot cannot continue without an answer. The app then shows that the bot waits for you. - `send_to_agent`: message another bot, or post into a room that the bot is in. In a team turn, the target is the team itself, or a room that the bot is in when the post wakes no one (a post wakes only the members it @mentions). - `target_id`: the id of a bot, or of a room. - `message`: at most 8000 characters. - `priority`: true wakes the bot now. False delivers at its next turn. Room posts always land at once. - `files`: room posts only, up to 4. - `read_room`: read the latest messages of a room that the bot is in, oldest first. - `room_id`. - `limit`: 1 to 50, default 20. - `before_seq`: page back from the oldest `#seq` that the bot has. - Reading does not change what the bot's next room turn shows as new. - `recall_memory`: search the bot's memory and the shared user memory. - `query`: facts that share more words rank first. - `scope`: `agent`, `user`, `team` (only in a team turn) or `all` (default). In a team turn, `all` includes the team's facts. - `limit`: 1 to 50, default 20. - `update_memory`: write or forget a fact. - `action`: `write` or `forget`. - `fact`: for forget, the exact recorded text. - `scope`: `agent` (default), `user` or `team` (only in a team turn). - `tier`: `profile`, `log` (default) or `note`. - `replaces`: the exact text of a fact that the new fact replaces. - `update_routine`: manage the bot's own routines. - `action`: `create`, `update`, `pause`, `resume`, `delete`, `run_now` or `list`. - `id`: required except for create and list. - `name`, `prompt`, `schedule`: required for create. - `update_skill`: manage the skills that all bots share. - `action`: `write`, `delete` or `list`. - `name`, `description`, `body`: required for write. - `id`: required for delete. - `find_artifacts`: list the artifacts, newest updated first, without their content. - `kind`: `plan`, `task`, `issue`, `review`, `report` or `note`. - `author`: `me`, `user` or a bot id. - `tag`: one exact tag. - `query`: text in the title or content, ignoring case. - `limit`: 1 to 50, default 20. - `read_artifact`: read one artifact by `id`, with its content inside an `` wrapper. - `update_artifact`: save, change or delete an artifact. - `action`: `write` or `delete`. - `id`: leave it out to create an artifact. Required to change or delete one, which only its author can do. - `kind`, `title`, `content`: required to create. When the bot changes an artifact, a field that it leaves out stays the same. - `tags`, `links`: replace the old tags or links. - `update_profile`: change the bot's own `name` or `instructions`. A field that the bot leaves out stays the same. - `create_setup`: Builder only. Create new bots, teams and rooms after the user confirms them on a card. See the Builder part of Bots and rooms. - `bots`: each with `name` and `instructions`, and optionally `model` and `effort`. - `teams`: each with `name`, `purpose`, `members` (bot names), `lead` (a member's name; the first member when empty), `agreement`, and optionally `default_model` and `default_effort`. - `rooms`: each with `name`, `purpose` and `members`. - `update_board`: work on the team's board, only in a turn in the team. See the Teams part of the docs above. - `action`, for tasks: `send_task`, `take`, `order`, `ask`, `answer_task`, `later`, `pass_on`, `push_back`, `propose`, `make_task`, `close`, `approve`, `stop` and `withdraw`. For steps: `create`, `assign`, `submit`, `block`, `unblock`, `drop`, `pass` and `changes`. Also `list`. - `goal_id`: the task, for `take`, `ask`, `answer_task`, `later`, `pass_on`, `push_back`, `close`, `approve`, `stop` and `withdraw`, and for `changes` on a done task. `order` takes no `goal_id`: its `order` field lists the ids of every task in the plan. - `item_id`: the step, for every step action except `create`. - `team_id`: for `send_task` and `pass_on`, the other team. - `text`: the task in a line, the result, a question, an answer, a reason or a note. `brief`, `title`, `acceptance` and `depends_on` are for `send_task`, `propose`, `make_task` and `create`. - `position`: for `take`, the task's place in the plan. `order`: for `order`, the ids of every task in the plan. - `until`: for `later`, when the task returns to the inbox, at most 7 days ahead. - `waits_for`: for `send_task`, the id of your own step that waits on the task. `artifact_ids`: artifacts the other team needs. - `turn_ids`: for `make_task`, the ids of the team's turns that were on no task. - `owner_id`, `reviewer_id`: for `create` and `assign`, by the lead only. - `approve`: internal. Claude Code calls it through `--permission-prompt-tool` to show an approval card. ## Local HTTP API The web UI uses this API. It is local and not versioned, so it can change between releases. Rules for every request: - The server answers only on a loopback address. A request whose `Host` is not loopback gets 403, to block DNS rebinding. - A request with an `Origin` header from another origin gets 403, to block cross-site request forgery. - Every `POST` under `/api/` must have `Content-Type: application/json`, or it gets 415. - A request body has at most 1 MiB. A message body may also carry its files, up to 100 MB before base64. - Every write under `/api/`, that is any method other than `GET`, `HEAD` or `OPTIONS`, must carry Handful's key, or it gets 401 with `{"error": "This needs Handful's key."}`. The key is the content of the file `ui-key` in the data folder (`~/.handful/ui-key`, or `/ui-key` with `-data`). A program sends it in the header `X-Handful-Key`. A browser sends it in the cookie `handful_key`, which a browser gets after you paste the key once on the key page, and which Handful.app sets itself. Reads need no key. The one write without the key is `POST /api/key`, which checks a pasted key and sets that cookie. - An error is JSON: `{"error": ""}`. Status 400 means invalid input, 404 not found, and 409 a conflict, for example an approval that is no longer pending. For example, to send a message to a bot from a script: ```sh curl -s -X POST http://127.0.0.1:7777/api/bots//messages \ -H "X-Handful-Key: $(cat ~/.handful/ui-key)" \ -H 'Content-Type: application/json' \ -d '{"text": "Hello", "clientNonce": ""}' ``` Bots, rooms and teams are the same resource. `kind` is `agent` for a bot and `room` for a room or a team. A team also has `team`. Bots: - `GET /api/bots`: all bots, rooms and teams. Each has `id`, `kind`, `name`, `instructions`, `role` (`builder` for Builder; absent for other bots), `members` (rooms), `team` (teams: `leadId`, `agreement`, `decisionMinutes`, `dailyBudget` and the other team settings), `attention` (`approval`, `question`, `goal` for a team with a paused task, or `model` for a model that needs a look), `running`, `tasks`, `createdAt` and `updatedAt`. - `POST /api/bots` with `{"name", "instructions"}`: create a bot. Returns 201 and the bot. - `GET /api/bots/{id}`: one bot or room. - `PATCH /api/bots/{id}` with `{"name"?, "instructions"?}`: change a bot. - `DELETE /api/bots/{id}`: stop the bot's running turn, then delete it. Returns 204. - `PUT /api/bots/order` with `{"ids": [...]}`: set the order of one sidebar section: all bots, all teams or all plain rooms. Room members answer in the order of the bots section. Chats: - `GET /api/bots/{id}/entries?after=`: the entries of a chat that changed after `rev`, in `rev` order, at most 1000 at a time. Start with `after=0`. - `POST /api/bots/{id}/messages` with `{"text", "clientNonce", "media"?}`: send a message to a bot or a room. - `clientNonce` is required, 1 to 100 characters, and unique for each message. - `media` is a list of base64-encoded files. - The response is 202 with `{"turnId", "duplicate": false}`. A repeated `clientNonce` starts no second turn: it returns 200 with the first turn's id and `"duplicate": true`. The same `clientNonce` with other text returns 409. - `GET /api/media/{name}`: a file of a message. - `POST /api/entries/{entry id}/reactions` with `{"emoji", "on"}`: put a reaction on a `user`, `bot` or `agent` entry (`"on": true`) or take it off (`"on": false`). `emoji` is one of 👍 👎 ❤️ 😂 👀 ✅. Returns 200 and the entry. Setting a reaction that is already on, or removing one that is off, changes nothing. Returns 400 for another emoji or another kind of entry, and 404 for an unknown entry. A reaction wakes no bot. - `POST /api/bots/{id}/interrupt`: stop the running turn. Returns `{"stopped": true|false}`. - `POST /api/bots/{id}/clear`: start a fresh context. Returns 204, or 409 while the bot works or has background tasks. - `POST /api/bots/{id}/compact`: queue a compaction after the current turn. Returns 202 with `{"queued"}`. Entries: - Each entry has `id`, `botId`, `seq` (its place in the chat), `rev`, `kind`, `turnId`, `body` and `createdAt`. - An entry can change after it is created, for example when an approval is answered. Each change gives it a new, higher `rev`, so update entries by `id`. - Kinds: - `user`: your message. `body` has `text` and `media` (file names for `GET /api/media/{name}`). - `bot`: a bot's message. `body` has `text`, `fallback`, `needsReply` and `media`. In a room, it also has `speakerId` and `speakerName`. - `agent`: a direct message between bots. `body` has `direction` (`in` or `out`), `peerId`, `peerName`, `text`, `priority`, and for `in`, `held`. - The bodies of `user`, `bot` and `agent` entries can also have `reactions`, your emoji on the message, in the order 👍 👎 ❤️ 😂 👀 ✅. The key is absent when the message has no reactions. - `approval`: an approval card. `body` has `tool`, `input`, `status` (`pending`, `allowed`, `denied` or `expired`), `decision`, and in a room `bot`. - `event` and `activity`: turn events and the tools that a bot used. Rooms: - `POST /api/rooms` with `{"name", "instructions", "memberIds"}`: create a room. Returns 201 and the room. - `PUT /api/rooms/{id}/members` with `{"memberIds"}`: set the members. - Send a message to a room with `POST /api/bots/{room id}/messages`. Teams and tasks: - `POST /api/teams` with `{"name", "instructions", "memberIds", "leadId"?, "agreement"?, "defaultModel"?, "defaultEffort"?}`: create a team. Returns 201 and the team. `instructions` is its purpose. Without `leadId`, the first member leads. - `PATCH /api/teams/{id}` with any of `{"leadId", "agreement", "defaultModel", "defaultEffort", "decisionMinutes", "dailyBudget", "inboxMax", "inboxPerSender"}`: change a team. The lead must be a member. `dailyBudget` is the turns the team may start in a day; 0 is no limit. - Set the members with `PUT /api/rooms/{team id}/members`, and change the name or the purpose with `PATCH /api/bots/{team id}`. - `GET /api/teams/{id}/board`: the team's work: `goal` (the Now task, or the latest one; null when there is none), `items` (its steps), `inbox`, `plan`, `done`, `stopped`, `sent`, `handedOver` (tasks the team sent to other teams), `offTask` (turns on no task in the last hour), `budget` (the team's turns today) and `teams` (names of the teams that appear). - `POST /api/teams/{id}/tasks` with `{"text", "brief"?, "artifactIds"?, "startNow"?}`: send the team a task as the user. It lands in the team's inbox, or at the top of the plan with `startNow`. - `POST /api/teams/{id}/tasks/from-turns` with `{"text", "brief"?, "turnIds"}`: make a task of 1 to 50 of the team's turns that were on no task. - `GET /api/teams/{id}/off-task`: the team's turns on no task. `POST /api/teams/{id}/off-task/dismiss` puts them away. - `POST /api/teams/{id}/plan` with `{"order"}`: set the order of the plan. - `POST /api/teams/{id}/budget/wait`: mark the team's pause at its daily limit as seen, so that Needs you stops asking about it until midnight. - `POST /api/tasks/{id}/take` with `{"position"?}`: take a task from the inbox into the plan. - `POST /api/tasks/{id}/push-back` with `{"text"}`, `POST /api/tasks/{id}/answer` with `{"text"}` and `POST /api/tasks/{id}/changes` with `{"text"}`: push a task back with a reason, answer a lead's question, or ask for changes to a done task. - `POST /api/tasks/{id}/pin` and `DELETE /api/tasks/{id}/pin`: pin a task to its place in the plan, or release it. - `POST /api/tasks/{id}/stop`, `/reopen`, `/withdraw`, `/dismiss` and `/approve`: stop a task, reopen a stopped one, take back a task that the team sent, put away a pushed-back task, or approve a done one. - `GET /api/tasks/{id}/events` and `GET /api/items/{id}/events`: the history of a task and of a step. - `GET /api/needs-you`: what only the user can settle: results to approve, decisions that a lead missed, tasks that are stuck, open questions and budgets that ran out. Memory, routines, approvals and permissions: - `GET /api/bots/{id}/memory`: the facts that the bot sees outside teams, or for a team id its team facts and the user facts. Each has `id`, `scope`, `tier`, `fact`, `learnedBy` (`user` or a bot id) and `createdAt`. - `POST /api/bots/{id}/memory` with `{"scope", "tier", "fact"}`: add a fact as the user. Returns 204. For a team, `scope` is `team` or `user`. - `DELETE /api/memory/{id}`: delete a fact. - `GET /api/bots/{id}/routines`: the bot's routines. Each has `id`, `botId`, `name`, `prompt`, `schedule`, `enabled`, `nextRunAt`, `lastRunAt`, `lastRunBy` and `createdAt`. - `POST /api/bots/{id}/routines` with `{"name", "prompt", "schedule"}`: create a routine. Returns 201. - `PATCH /api/routines/{id}` with `{"name"?, "prompt"?, "schedule"?, "enabled"?}`: change, pause or resume a routine. - `POST /api/routines/{id}/run`: run a routine now. Returns 202 with `{"turnId"}`. - `DELETE /api/routines/{id}`: delete a routine. - `POST /api/approvals/{entry id}` with `{"decision": "once" | "always" | "deny"}`: answer an approval card. Returns 204, or 409 when the card is no longer pending. `always` on a card of `create_setup` returns 400. - `GET /api/bots/{id}/grants`: the tools that the bot may use without asking. - `DELETE /api/bots/{id}/grants/{tool}`: remove one of them. Artifacts: - `GET /api/artifacts?kind=&author=&tag=&q=&limit=`: the artifacts that match every filter that is set, newest updated first, without `content`. Each has `id`, `kind`, `title`, `authorId` (`user` or a bot id), `tags`, `links`, `createdAt` and `updatedAt`. `q` matches text in the title or content, ignoring case. `limit` is 1 to 50, default 20; a larger limit returns at most 50. - `GET /api/artifacts/{id}`: one artifact, with its `content`. - `POST /api/artifacts` with `{"kind", "title", "content", "tags"?, "links"?}`: create an artifact as the user. Returns 201 and the artifact. - `PATCH /api/artifacts/{id}` with any of `{"kind", "title", "content", "tags", "links"}`: change an artifact, whoever wrote it. Fields that you leave out stay the same. - `DELETE /api/artifacts/{id}`: delete an artifact, whoever wrote it. Returns 204. Skills and connectors: - `GET /api/skills`: the skills, with `id`, `name` and `description`. - `GET /api/skills/{id}`: one skill, with its `body`. - `DELETE /api/skills/{id}`: delete a skill. - `GET /api/connectors`: the connectors from `connectors.json`. - `POST /api/connectors/{name}/connect`: start a sign-in. Returns `{"url"}` to open in a browser. The sign-in returns to `/oauth/callback`. - `DELETE /api/connectors/{name}/token`: sign out and delete the tokens. Settings, session and phone access: - `GET /api/version`: the server's version. `GET /api/defaults`: the default model and effort. - `GET /api/settings` and `PATCH /api/settings`: the settings of `config.json`. `PUT /api/settings/file` replaces a broken file. `POST /api/restart` restarts the server when it is idle, and `DELETE /api/restart` cancels that. - `POST /api/drain` and `DELETE /api/drain`: stop and resume the start of new turns, for an update. - `GET /api/session`: whether the request carries Handful's key, and the key file's path when it does not. `POST /api/key` with `{"key"}` checks a pasted key and gives the browser the key's cookie. - `GET /api/phone`, `POST /api/phone/on` and `POST /api/phone/off`: phone access, which is off by default. - `POST /api/phone/pairing`, `PATCH /api/phone/pairing` and `POST /api/phone/pairing/{id}`: start a pairing, set its scope, and approve or deny it. - `GET /api/phone/sessions`, `DELETE /api/phone/sessions` and `DELETE /api/phone/sessions/{id}`: list the paired phones, revoke all of them, or revoke one. Event stream: - `GET /api/events` is a server-sent event stream of every change. - It starts with `event: hello`. If new turns wait for a usage limit, a `pause` event follows. - Every other event is a `data:` line with a JSON object. Its `type` is one of: - `entry`: an entry was added or changed. It has `botId` and `entry`. - `state`: a bot started or stopped working. It has `botId` and `running`. - `tasks`: a bot's background tasks changed. It has `botId` and `tasks`. - `bots`: the list of bots and rooms changed. Fetch `GET /api/bots` again. - `pause`: `pausedUntil` is when turns start again. It is absent when the pause ended. - A comment line, `: ping`, comes every 20 seconds. - A client that falls behind is disconnected. After each connect, fetch the entries after the highest `rev` that you have. Examples: ```sh curl -s http://127.0.0.1:7777/api/bots curl -s -X POST http://127.0.0.1:7777/api/bots//messages \ -H "X-Handful-Key: $(cat ~/.handful/ui-key)" \ -H 'Content-Type: application/json' \ -d '{"text": "@here what is the status?", "clientNonce": ""}' curl -s 'http://127.0.0.1:7777/api/bots//entries?after=0' curl -N http://127.0.0.1:7777/api/events ```