issuebot starts a coding agent for tasks on a board. For each task, issuebot does these steps:
- It finds a task that the board gives to your agent identity.
- It claims the task.
- It prepares a workspace.
- It starts an agent CLI (Claude Code) to do the task.
- It publishes the result.
issuebot has five plugin axes:
| Plugin axis | What it sets | Available |
|---|---|---|
| source | where the tasks come from, and what a run can report | issuebear |
| workspace | where issuebot prepares the working copy | git, folder |
| environment | which machine the agent runs on | local, railway |
| harness | which agent CLI issuebot starts | claude, ollama |
| sink | where issuebot publishes a result | github |
Each plugin can add configuration keys, issuebot doctor checks, and commands.
issuebot --help shows the commands in this installation. The installed
issuebear source supplies the tasks for all connections. All connections use
the configured harness. You can make one connection for each board.
Connections can use different workspace, environment, and sink plugins.
- Install
- Quickstart
- How a task runs
- Configuration
- Connections
- Workspaces
- What a run can report
- Sinks
- Environments
- Harnesses
- Monitor a run
- Skills, plans and confirmation
- Workspace bootstrap
- Housekeeping
- Security
Each GitHub Release has an issuebot wheel for one stable version. With no
version, the installer uses GitHub's latest release. Give a stable X.Y.Z
version to install that specified release.
curl -fsSL https://github.com/teamwebhq/issuebot-public/releases/latest/download/install.sh | shcurl -fsSL https://github.com/teamwebhq/issuebot-public/releases/download/v0.2.0/install.sh | sh -s -- 0.2.0
Before you start, make sure that the machine has curl. If necessary, the
script installs a pinned uv. Then, uv installs Python if necessary.
The script does these steps:
- If you give no version, it gets the latest GitHub Release version.
- If you give a version, it makes sure that the version has the stable
X.Y.Zformat. - It selects the versioned wheel for that release.
- It installs a pinned
uvif the machine has nouv. - It installs issuebot in
/usr/local/bin, or in~/.local/binif/usr/local/binis not writable. To use a different directory, setISSUEBOT_BIN_DIR. The script stops if that directory is not in yourPATH. - It makes sure that the installed command reports the selected version.
You can run the script again. To update issuebot, run the script for the latest
release. To install a different release, give its stable X.Y.Z version.
issuebot --help # make sure issuebot is on your PATH
issuebot version # the version of the package# 1. Connect this installation to your board server
issuebot init
# 2. Make a connection: a board, a place to work, and where results go
issuebot connect
# 3. Claim and work tasks until you push Ctrl-C
issuebot listenRun issuebot init to make the configuration file.
The command writes ~/.config/issuebot/config.toml. It prompts for the Issuebear
URL, the agent PAT, and the harness. It also prompts for the harness executable
path. The command finds the API and MCP endpoints from the Issuebear URL. If it
cannot find them, it prompts for each endpoint. Before it writes the file, the
command makes sure that the PAT can get work.
For a harness that runs Claude Code (claude and ollama), issuebot init
tries to add the board MCP server to Claude Code. The command uses the user
scope. This setup lets you speak to the board directly. To skip this setup, use
the --skip-harness-setup option.
Run issuebot connect without --name and --board to start the wizard. The
wizard does these steps:
- The
issuebearsource prompts for the organization, project, and board. - The wizard gives a default name for the connection.
- It shows the installed environments and prompts you to select one.
- It prompts for the settings of the selected environment. For
local, it prompts for the mode and the source of the working copy. Inboardmode andbuildmode, it prompts fornone,branch, orworktree. It also prompts for the folder or clone URL. - It prompts for update-base only for a task branch. Then, it prompts for plan confirmation and done-mode.
- For each sink, it prompts you to select
no,required, orbest-effort.
You can also use flags with issuebot connect. Refer to
Connections.
In script mode, give the board id with --board. The issuebear source
uses the agent PAT in the configuration. It gets only tasks that the board
gives to the agent identity of that PAT.
Run issuebot listen with no names to monitor all configured connections. Give
one or more names (issuebot listen myproj otherproj) to monitor only those
connections. The command monitors the configuration file. Thus, issuebot uses
a change from issuebot connect immediately. If the command cannot read a
change, it reports the error and continues with the active connections.
Run issuebot doctor to examine an installation. First, it makes sure that the
PAT can read the list of outstanding tasks. It stops if the PAT cannot read the
list. Then, it examines the harness executable. It also does the available
plugin checks for each connection. All checks after the PAT check are warnings.
- Get work — issuebot reads the work that is outstanding for the agent: the tasks, and the mentions. The tasks list shows the tasks that stay assigned to the agent, and the board pool work for which the agent is responsible. Each read changes nothing at the source, and each read gives the same items again until a claim removes them. Thus, work that issuebot cannot run now is not lost: it is in the next read.
- Claim the work — issuebot claims each work item. A claim acknowledges the work and removes it from the next read. If a different listener has the claim on a task, issuebot continues to the next work item. A mention claim gives the non-locking run in which the agent writes its answer.
- Set the outputs — issuebot sets the output kinds that the run can report. The agent prompt shows only these output kinds. Refer to What a run can report.
- Prepare the workspace — issuebot makes the working copy or uses it again.
- Run the bootstrap — issuebot runs the
.issuebear.tomlbootstrap. - Start the agent — the harness starts the agent in the workspace. The source supplies the board MCP server with the agent PAT.
- Monitor the run — issuebot shows a live feed. It sends heartbeats if the source supplies a run ID.
- Read the response — the agent writes JSON to the path in the
$ISSUEBOT_RESPONSEvariable. issuebot reads this file. The run fails if the file is missing or incorrect. - Commit changes — if the run permits
changes, the workspace commits the changed files. The message is the task reference and the summary that the agent wrote in itschangesoutput: the first line of that summary is the subject, and the remainder is the body. A run that reports nochangesoutput commits with the task reference only. - Record changes — issuebot gets the change data from
git. - Push the branch — issuebot pushes only if the branch head moved,
pushistrue, and there is anoriginremote. If the push fails for a reason that issuebot does not know, it tries the push again, to a maximum of three attempts. It does not try again for a reason that it knows is terminal, for example a non-fast-forward or a protected branch. - Do a check of the response — issuebot makes sure that the run permits
each output kind. It makes sure that each output has its necessary field. It
permits a maximum of one decision. It rejects a
changesoutput if the branch did not move. - Send deliverables — issuebot sends each deliverable to each sink that accepts its kind. It uses the sink sequence in the connection. All sinks get their applicable deliverables.
- Sink results — issuebot fails the run if a required sink
fails. It does not apply the decisions. A
best-effortsink failure does not fail the run. - Apply decisions — issuebot sends each decision to the source.
- Report sink results — the source adds a task comment if there is a sink result.
- Release the run — issuebot releases the run as done or failed. The release data includes result text when applicable.
The agent uses task comments for answers and a summary of its work. After it
uses ask_questions, the agent adds one short comment. This comment only says
that the questions are in the form. It does not write the questions again.
After the agent completes its work, issuebot adds a comment if a sink has a result. Thus, the comment can include a pull request URL. If there are no sink results, issuebot adds no comment.
If the agent cannot continue without an answer, it uses ask_questions. This
command puts the questions in a question form and stops the run until you
answer. The agent adds one short task comment. This comment only says that the
questions are in the form. It does not write the questions again in the
comment.
The response file reports a needs_input output and its question. issuebot
sets the task to the awaiting-input status. Do these steps to continue:
- Write your answer in a task comment.
- Assign the task to the agent again.
The next run uses the task workspace again. If session resumption is active, the next run also continues the same agent conversation. Refer to Session resumption.
The configuration file is at ~/.config/issuebot/config.toml. Set the
$ISSUEBOT_CONFIG variable to use a different path. The file contains your
credentials. Thus, issuebot writes it with the 0600 permissions. Keep these
permissions. The board server does not read this file.
The configuration has a small number of core keys. It also has keys for the five plugin axes. The plugin keys have two structures:
- A table with the name of the plugin, for example
[git],[claude], or[connections.railway] - A bare key that the plugin claims, for example
git_initorboard.
If no installed plugin claims a key, issuebot does not load the configuration.
Core keys:
| Key | Default | Meaning |
|---|---|---|
harness |
— | Agent CLI that does the work. Necessary only if more than one harness is installed |
max_concurrent |
1 |
Maximum number of tasks that issuebot does at the same time for each harness, for all connections. At 2, issuebot does two claude tasks and two ollama tasks at the same time. Restart issuebot after a change |
task_timeout_minutes |
not set | Maximum time for one run. If not set, there is no maximum |
connections |
[] |
Connections, with one array-of-tables entry for each connection |
The harness key and the executor key of each connection have no default. If
you do not give a name, issuebot uses the one installed harness or environment.
If more than one is installed, issuebot reports this:
no harness named, and 3 are installed — set harness = "…" (known: claude, fake, ollama)
connection 'web': no environment named, and 2 are installed — set executor = "…" (known: local, railway)
More than one harness and more than one environment are installed in this
build. Thus, write the two keys. issuebot connect writes executor. In script
mode, it stops if you do not give --executor and more than one environment is
installed. issuebot init writes harness. Refer to
Environments.
An update control makes issuebot install itself again. It downloads install.sh
from the latest GitHub Release and runs it. There is no configuration key for
this: issuebot finds the command when the update control arrives. Thus, an
install always uses the installer of the build that runs.
An update waits for active tasks. issuebot stops new task claims and lets the active tasks complete. Then, it does the update. The update control gives each listener 30 minutes to complete its active work.
If a listener does not complete, issuebot does not update. It reports the problem. The runner continues with the installed version.
A restart control stops active agents and does not wait.
A minimal configuration:
harness = "claude"
# The global table of the issuebear source: the board and the credential.
[issuebear]
api_url = "https://issuebear.example.com/api"
mcp_url = "https://issuebear.example.com/mcp"
pat = "ib_pat_xxx"
[[connections]]
name = "myproj"
board = "board-1"
folder = "/home/me/code/myproj"
executor = "local"A configuration with a git workspace, a sink and three global tables:
harness = "claude"
max_concurrent = 2
task_timeout_minutes = 30
[issuebear]
api_url = "https://issuebear.example.com/api"
mcp_url = "https://issuebear.example.com/mcp"
pat = "ib_pat_xxx"
install_name = "laptop" # the name of this installation on the dashboard
telemetry_interval_seconds = 15
[claude]
command = "/Users/you/.claude/local/claude" # empty or absent = find it on the PATH
resume_sessions = true
[git]
worktree_root = "/var/tmp/issuebot/worktrees" # default: <state dir>/worktrees
clone_root = "/var/tmp/issuebot/clones" # default: <state dir>/clones
[github]
summary_model = "claude-haiku-4-5" # the model for the pull request description
[[connections]]
name = "web"
board = "board-1"
folder = "/home/me/code/web"
executor = "local"
git_init = "worktree" # the keys of git are on the connection
update_base = "merge"
confirm = true
done = "review"
sinks = ["github"]Some settings name a path on this machine: [claude] command, [git] worktree_root and [git] clone_root. A task that runs in a sandbox does not
get them — the path does not exist there — so the sandbox finds claude on its
own PATH and uses its own state directory. Thus one configuration can have a
local connection with a full path and a railway connection at the same time.
issuebot rejects a configuration that it cannot use. It reports the problems that it finds together. issuebot examines these items:
- Unknown keys, with a possible correct key
- A key that a plugin claims when the connection does not use that plugin, for
example
[connections.railway]with thelocalexecutor - The
source, theexecutor, and each sink, which must be installed - The type of each setting
- Rules between fields, for example the permitted combinations of
git_init,repo, andfolder - Rules for sinks, for example a pushed-branch sink with
push = false.
If issuebot cannot use a configuration, the command reports the problems after
Config error in <path>:. It exits with code 1. If there is no file, the
command tells you to run issuebot init first.
A connection has one source → workspace → environment → sinks structure. The
core fields are name, source, folder, executor, and sinks. In a correct
configuration, a plugin claims each other field.
Use issuebot connect with flags in a script:
issuebot connect --name myproj --board <board-id> --folder /path/to/repo \
--isolation worktree --update-base merge --confirm yes --sinks github| Flag | Writes | Notes |
|---|---|---|
--name |
name |
Use with --board. Without --name and --board, the wizard starts |
--board |
board |
the key of the issuebear source |
--folder |
folder |
Absolute path. The folder must be a directory |
--repo |
repo |
Clone URL. Makes a new clone for each task. Do not use with --folder |
--isolation |
git_init |
What to make in the working copy: none (default) makes nothing, branch, or worktree. Refer to Workspaces |
--branch-prefix |
branch_prefix |
Default issuebot/. If you change the default, use branch or worktree isolation |
--update-base |
update_base |
none (default), rebase or merge |
--mode |
mode |
board (default), build or respond. Refer to What a run can report |
--done |
done |
review (default) or complete |
--confirm |
confirm |
yes (default) or no. If yes, the agent waits for approval of the plan before it writes code |
--executor |
executor |
Installed environment. --help shows the list. Necessary only if more than one is installed |
--sinks |
sinks |
Repeatable and in sequence. NAME[:best-effort] |
--set |
a plugin setting | Repeatable. <name>.<key>=<value> |
Use --set for plugin connection settings that do not have flags:
issuebot connect --name web --board <board-id> \
--repo https://github.com/org/web.git --isolation branch \
--executor railway \
--set railway.environment_id=<env-id> \
--set railway.token=<token> \
--sinks githubissuebot connect --help shows each key that you can use with --set in this
installation. It shows the permitted values and descriptions that each plugin
supplies.
issuebot examines each value immediately and rejects an incorrect value. For
example, it can report Input should be 'isolated' or 'private'. If a different
flag writes the same key, issuebot rejects the value. It also gives the flag
name, for example --set git.git_init=….
Before it writes, issuebot connect uses the configuration rules to examine
the new connection. It also rejects a second connection to a board that this
agent has.
issuebot disconnect --name <name> removes a connection from the configuration
and tells the server. If there is no server response, the connection stays
removed from the configuration.
issuebot connections shows a summary of each connection:
$ issuebot connections
2 connections:
myproj · board board-1 · /home/me/code/myproj
mode board
isolation worktree
done review
confirm yes
update-base merge
branch-prefix issuebot/
sinks github
docs · board board-2 · /home/me/code/docs
mode respond
isolation none
done review
confirm yes
update-base none
branch-prefix issuebot/
sinks none
You make two selections for the workspace. Each connection has the two selections:
| Setting | Answers | |
|---|---|---|
| Working-copy source | folder or repo |
a folder on this machine, or a clone from a URL |
| Git preparation | git_init |
a task branch, a task worktree, or no task branch |
You can use each git_init value with folder or repo.
Set one of these working-copy source keys:
folder = "/path/to/repo"— issuebot uses this folder and does not clone it. For a git workspace, this folder must be a git repository.repo = "https://github.com/org/x.git"— issuebot clones this URL. Withgit_init = "branch"or nogit_init, it keeps one clone for each task. The clone root is<state dir>/clonesor[git] clone_root. Use an HTTPS URL for a GitHub repository: issuebot gives each clone theghCLI as its credential helper, andghis the only GitHub credential an executor holds. An SSH URL needs a key and a known-hosts entry that an executor does not get.
With repo and git_init = "worktree", issuebot keeps one clone for all task
worktrees. It makes the task worktree in <state dir>/worktrees or
[git] worktree_root.
If you set the two source keys, issuebot does not load the configuration.
The board sends the repository of each task's project with the task. If that
repository is not the repo of the connection, issuebot fails the run and puts
both URLs on the task. It does not change the configuration. Set repo to the
repository of the project, or connect the project to the correct repository.
issuebot makes no check for a connection with a folder, or for a task whose
project has no repository.
Use git_init to select the git preparation:
- If
git_initis not set, issuebot uses the current branch. It does not commit or push changes. Thus, the run cannot report achangesoutput. git_init = "branch"— issuebot checks out a task branch in the working copy. The first task branch has the default nameissuebot/<ref>.git_init = "worktree"— issuebot makes a worktree on a task branch. The first task branch has the default nameissuebot/<ref>.
The git workspace also uses these keys:
branch_prefix— sets the task branch prefix. The default isissuebot/.update_base— usesnone,rebase, ormerge. If there is anoriginremote, a rebase or merge uses its default branch. The default isnone.push— controls the push after a commit. The default istrue.
Use branch_prefix and update_base only with git_init. issuebot rejects
these keys when git_init is not set.
If a run permits changes, issuebot commits all changed files. It pushes only
if push is true and there is an origin remote. A sink can then
use the pushed branch.
If push is false, the work stays on this machine. issuebot rejects this
setting for a sink that uses a pushed branch.
A connection with only folder uses that folder and does not commit changes.
Set folder_init = "copy" to make a temporary copy for each task.
Use the issuebot git group for git worktrees and clones. Run these commands
to see the applicable list and prune commands:
issuebot git --help
issuebot git worktree --help
issuebot git clone --helpRun these commands to list the managed working copies:
issuebot git worktree list
issuebot git clone listUse a task ref or a selection option with a prune command. The commands do not
prune a dirty or unpushed working copy unless the command has the --force
option.
issuebot git worktree prune ISS-42
issuebot git worktree prune --all
issuebot git worktree prune --merged
issuebot git worktree prune ISS-42 --force
issuebot git clone prune ISS-42
issuebot git clone prune --all
issuebot git clone prune --merged
issuebot git clone prune ISS-42 --forceThe worktree --merged option uses git branch ancestry. The clone --merged
option uses gh pr view.
A task can have more than one run. issuebot uses the applicable task branch again:
- The working copy is available. issuebot uses it again. It fetches a clone
on this machine. For a connection with a task branch, it fast-forwards the
branch from
origin. - The working copy is not available. issuebot makes it again. If the task
branch is on this machine or on
origin, issuebot continues from that branch.
If the default branch contains a previous task branch, issuebot uses the next
branch name. It also uses the next name if a previous branch has a merged pull
request. For example, it can use issuebot/<ref>-2.
If issuebot cannot fast-forward the task branch, or if an update_base rebase
or merge causes a conflict, the run continues. issuebot tells the agent to
reconcile the branch before the task. The instructions ask for the operation
that update_base names. If the agent cannot reconcile the branch safely, it
writes a comment on the task and stops.
If the run permits changes, a preparation or bootstrap failure fails the run.
For other runs, issuebot uses the connection folder after a preparation
failure. Without a connection folder, the run fails.
For these other runs, a bootstrap failure does not fail the run. issuebot starts the agent without the data that the bootstrap adds.
The agent writes one JSON document to the path in the $ISSUEBOT_RESPONSE
variable. The document is {"outputs": [...]}. It can contain no outputs or
one or more outputs of four kinds.
| Kind | Contains | Category |
|---|---|---|
changes |
summary |
deliverable |
answer |
text |
deliverable |
needs_input |
question |
decision |
handoff |
assignee, note |
decision |
issuebot sends each deliverable to each sink that accepts its kind. It sends a decision to the source. A run can report a maximum of one decision.
Two conditions decrease the output kinds that a run can report:
- The kind of work. An assignment can report all four kinds. An @mention
cannot report
changes. A run that responds instead of builds also cannot reportchanges. - The workspace. A workspace without a task branch cannot
report
changes.
The agent prompt shows only the permitted kinds. issuebot rejects each other kind.
The mode setting controls the first condition. It has three values:
| Value | What the connection does |
|---|---|
board (default) |
Each task does what its board column asks for: it builds, or it responds. A column that asks for nothing builds |
build |
Always build, whatever the column asks for |
respond |
Always respond, whatever the column asks for |
A board column also composes the prompt for the tasks in it. A connection in
board mode launches the agent with that prompt. When the column composes none,
and for the two overrides, the run uses the board's own instruction document for
build work or respond work. The mode does not select the workspace.
A response is not a sandbox. A run that responds can report only kinds other
than changes.
issuebot tells the agent not to change files and rejects a changes output.
The agent has its usual file tools and shell tools. Refer to
Security.
At the end of a run, issuebot tells the board what the run did: the agent's own summary, whether it changed and pushed code, the branch it made (pushed or not), and each pull request that a sink opened, with its draft state. The board shows this on the task. A sink reports the pull request that it opened or found; issuebot does not read a pull request out of a URL.
A sink publishes a deliverable. Sinks run on the controller and not in a sandbox. Thus, issuebot does not send sink credentials to a sandbox.
Set the sinks for each connection, in the sequence that they run:
issuebot connect --name web --board <id> --repo <url> --isolation branch \
--sinks githubA sink is required by default. If a required sink fails, the run fails and
issuebot does not apply its decisions. Add the :best-effort suffix to let the
task complete after a sink failure:
issuebot connect --name web --board <id> --folder /path --isolation branch \
--sinks github:best-effortIn the configuration, write sinks = ["github"] or
sinks = [{ name = "github", required = false }].
A sink receives only the output kinds that it accepts. The github sink
accepts only changes from a pushed branch. It does these steps:
- Do a check of change data — the sink makes sure that the branch head is different from its base.
- Find the repository — the sink uses the connection
repovalue. If there is norepovalue, it uses the checkoutorigin. - Do a check of the push — the branch must be on
origin. If the run could not push the branch, the sink stops and reports that the branch is not onorigin. - Do a check of the remote branch — the sink uses the GitHub compare API. The branch head must be after its base.
- Find an open pull request — the sink looks for an open pull request for the branch. The sink does this step before the description, because the description tells the reader what the pull request contains.
- Obey the step policy — the board step says what it wants done with the
branch. If the step asks for no pull request, and the branch has no open
pull request, the sink stops here and reports a success: the branch is on
originfor a later step to use. The sink writes no description, thus it does not use the harness. - Make the description — the harness makes the pull request title and body.
The sink tells the harness where the change is; the harness reads it. For a
new pull request, the change is the range from the base to the head of the
branch. For a pull request that is already open, the change is the whole
pull request. The harness follows the board's
writing-pull-requestsskill when the board sent one (see Skills, plans and confirmation). The[github] summary_modelkey sets the model. - Open or update the pull request — if the sink found no open pull request,
it opens one with
gh pr create. When the step asks for a draft, the sink runsgh pr create --draft. If the sink found an open pull request, it replaces the title and the body of that pull request. - Open a draft for review — if the pull request is a draft, and the step
does not ask for a draft, the sink makes it ready with
gh pr ready. The sink never does the opposite. A pull request that is ready stays ready, because a person can make it ready. - Ask for the reviews — the sink asks for each reviewer of the step with a
separate
gh pr edit --add-reviewercommand. It never gives the reviewers togh pr create: one name that is not a collaborator makes the full command fail, and the loss of the pull request is worse than the loss of a review request. If a request fails, the delivery is still a success, and the report gives the name.
Each run writes the whole description again. A second run on the same branch describes the pull request as it then is. This keeps the description correct for all of the work in the pull request. It also removes changes that a person made to the body. Write such notes in a review comment.
If the sink cannot update the description, the delivery is still a success: the
branch is on origin and the pull request is there to read. The delivery
report says that the description is not new.
The sink cannot use the harness if the harness is missing, fails, or gives
empty text. In these cases, the sink uses the changes summary and
git diff --stat data.
The GitHub API path includes the repository name. Each gh pr command uses
-R owner/name.
Install gh. Authenticate the gh CLI. issuebot doctor examines it.
A connection with no sinks keeps the work on the task branch.
The environment sets which machine the agent runs on. Set it with --executor
on each connection. There is no default, and two environments are available.
local — the agent runs as a subprocess on the machine that runs
issuebot listen, in the workspace of the connection. This environment has no
settings.
railway — each task gets a new
Railway sandbox. A sandbox is a temporary
Linux VM. issuebot makes it for the task and deletes it when the task ends.
issuebot listen stays on your machine and does these steps:
- It claims the task.
- It starts a sandbox.
- It aligns the sandbox to the controller's exact released distribution version.
- It runs
issuebot run-onein the sandbox. - It sends the output to your terminal.
- It gets the result.
- It deletes the sandbox.
Railway execution requires the controller itself to run from an installed, non-editable release wheel. A source checkout or editable installation is rejected before issuebot allocates a sandbox. Local execution remains available from a development checkout.
Note. Railway Sandboxes change frequently. Make sure that
issuebot railway build-templateoperates with your installedrailwayCLI.
- A Railway account with sandbox access, and a project and an environment for the sandboxes.
- The
railwayCLI on thePATHof the machine that runsissuebot listen. A runner that starts as a service (systemd, launchd, container exec) has a smallPATH. Then give the full path of the CLI with--set railway.command=/full/path/to/railway. - A Railway token for each connection, set with
--set railway.token=…. Thus one runner can use sandboxes in more than one project. A project token operates only in the environment that it was made for. Set--set railway.token_kind=project|accountto tell the CLI which variable to read the token from:RAILWAY_TOKENorRAILWAY_API_TOKEN. A connection with no token uses the variable that theissuebot listenprocess has. - A model credential for the harness, as a shared variable in that Railway environment. See Sandbox credentials.
GH_TOKEN, as a shared variable, only if your board lends no git credentials. See Sandbox credentials.
issuebot points to these shared variables, but does not read them. issuebot sends the board URLs and the agent PAT from your configuration.
Put each credential in a shared variable of the Railway environment, with the name that this section gives. issuebot points the sandbox at these names. A shared variable with a different name does not reach the sandbox.
The claude harness needs one of these two variables. Set one only.
Recommended: CLAUDE_CODE_OAUTH_TOKEN. This is your Claude subscription,
and it needs a Pro, Max, Team or Enterprise plan. To make the token:
- Run
claude setup-tokenon your own machine. Your browser opens. - Approve the access. The command prints the token.
- Copy the token. The command saves it nowhere.
- Put the token in a shared variable with the name
CLAUDE_CODE_OAUTH_TOKEN.
The token is good for one year. It belongs to the person who made it, and all
runs use the limits of that one subscription. Thus more max_concurrent tasks
can reach the limit and stop.
Alternative: ANTHROPIC_API_KEY. Make an API key in the
Claude Console. The API key
bills for each token, and it has no subscription limit, so it is the better
credential for a runner that a team shares.
Do not set both. Claude Code uses ANTHROPIC_API_KEY first, and then the
subscription token is dead weight.
Note. issuebot points the sandbox at both names. If your logs show an unresolved
${{shared.…}}value for the variable that you did not set, make that shared variable with an empty value.
GH_TOKEN is optional. A board with the GitHub App integration lends a
short-lived token for each run. The token has the identity of the app, and it
replaces GH_TOKEN in the sandbox. Then you do not have to set GH_TOKEN.
Set GH_TOKEN if your board lends no credentials. Without a token, the
sandbox cannot clone a private repository, and it cannot push. Give the token
these scopes: repo and read:org. If a run cannot borrow a token, the log
gives a warning and the run uses GH_TOKEN.
issuebot doctor gives a warning if the CLI is absent, or if a railway
connection has no token and the environment has no token.
A sandbox starts from a template with the tools that the agent needs: git,
curl, gh, chromium, and the exact release of the controller. Each build
step first looks for the tool, and installs it only if the Railway base image
does not have it. Today that image has all of them but chromium. Build the
template one time for each Railway project, while you run the controller from a
released wheel:
issuebot railway build-template # in the default project
issuebot railway build-template --connection web # with the token of a connectionThe name of the template is local to the CLI that builds it. Build the template
on the machine that runs issuebot listen, and with the same user. If you build
it elsewhere, the sandbox cannot find the template.
chromium is in the template so that an agent can open its own change in a
browser. The sandbox runs as root, and the sandbox of Chromium cannot start
as root. Thus the agent must give the --no-sandbox flag:
chromium --headless --no-sandbox --screenshot=/tmp/page.png http://localhost:3000Tell the agent this in the instructions of your board. issuebot cannot add the flag, because the agent selects its own browser tools and its own arguments. Puppeteer and Playwright have the same requirement, and each has its own way to give the flag and to use the browser of the system.
The wizard shows Railway when it asks where the tasks must run. For Railway, the wizard sets a cloned working copy on a task branch. Then it asks for the repository URL, the environment id, the network mode and the token of this connection. If the CLI is not ready, the wizard gives a warning but continues.
To configure the connection manually, or in a script:
harness = "claude"
max_concurrent = 3 # railway tasks at the same time, for each harness (default 1)
[issuebear]
api_url = "https://issuebear.example.com/api"
mcp_url = "https://issuebear.example.com/mcp"
pat = "ib_pat_xxx"
[[connections]]
name = "web"
board = "board-1"
executor = "railway"
repo = "https://github.com/org/web.git"
git_init = "branch"
done = "review"
sinks = ["github"]
[connections.railway]
environment_id = "<env-id>" # necessary
network = "isolated" # "isolated" (default) | "private"
token = "<token>" # absent = use the variable of the runner
token_kind = "project" # "project" (default) | "account"
command = "railway" # name on the PATH (default), or an absolute pathWith network = "private", the sandbox joins the private network of the
environment. Then the agent can get access to your services, for example
postgres.railway.internal. Use this mode only if a task needs those services.
A railway connection must have a [connections.railway] table with an
environment_id key. Without the key, the configuration does not load.
You can use local connections and railway connections in one configuration. The
executor key of the connection sets the route for each task.
- Warm starts — the first run for a project caches a checkpoint with the repository and the installed dependencies. Subsequent runs start from the checkpoint and fetch the latest commits.
- Concurrency — issuebot runs a maximum of
max_concurrenttasks for each harness at the same time, each one in its own sandbox. Your Railway plan can set a lower maximum. - Pause and resume — if a run ends with
needs_input, issuebot writes a checkpoint and deletes the sandbox. When the task comes back, the next run starts from that checkpoint. On theclaudeharness, the checkpoint also contains the agent conversation. - Version alignment — at the start, the controller asks the sandbox which distribution version of issuebot it has. If the versions differ, the controller installs its own exact GitHub Release wheel and verifies the sandbox again before starting work.
issuebot always deletes the sandbox when the task ends. Delete the checkpoints of paused tasks with these commands:
issuebot railway prune-checkpoints # more than 7 days old
issuebot railway prune-checkpoints --ttl-hours 24Caution. The Free plan of Railway sets a maximum idle timeout of 5 minutes for a sandbox. This is too short for an agent run. Use the Hobby plan or the Pro plan.
The harness starts one agent CLI. Select the harness at issuebot init, or set
the harness key in the configuration.
-
claude— Claude Code without a terminal (claude -p …). issuebot uses--strict-mcp-config, thus the agent has only the board MCP and not your other MCP servers. issuebot also uses--dangerously-skip-permissions, because an unattended runner cannot give approvals, and--output-format stream-jsonfor the live feed and the log. -
ollama— the same Claude Code, run against an Ollama model (ollama launch claude … -- …). Ollama is the program, thus[ollama] commandgives the path to Ollama and not to Claude Code. issuebot usesollama launch --yes, because the model menu needs a terminal. All the flags of theclaudeharness come after the--separator, thus the agent behaves the same.
If the board MCP server stops during a run, Claude Code cannot connect to it again. Thus issuebot stops the agent and starts it again from the same session. The new agent process connects to the board again. The log gives the reason.
issuebot init offers claude and ollama. A Codex harness (codex exec …)
exists in the codebase but is not offered or selectable — see
Skills, plans and confirmation for why. A
config left over from before this change that still says harness = "codex"
refuses to load; the error names the setting and tells you to change it to
claude.
A task from the board can ask for a harness or a model by name. The harness
the install configures is the default: a task that names a different harness
gets that one, if this install has it. If this install cannot run the named
harness, it uses its own and logs a warning that names both — a preference set
on a machine the board cannot see never fails a run. A task that runs on a
harness other than the install's starts a new agent session, because a stored
session belongs to the harness that started it. A requested model is
passed straight through to the --model flag of the harness with no
matching against anything; an unrecognised name is the harness's own error to
raise. On the ollama harness, the model names an Ollama model and goes to
ollama launch. If the board asks for no model, ollama launch selects one.
The CLI must be on your PATH. issuebot doctor examines it. If the CLI is
not on your PATH, give the path at issuebot init, or set the path in the
table of the harness:
# fragment: one table of a full configuration
[claude]
command = "/Users/you/.claude/local/claude"If the command key is not set, issuebot finds the name of the harness on the
PATH.
Each start of the agent writes a temporary MCP configuration file. The file
contains the MCP server of the board, with the name of your source. Thus the
board tools of the agent are mcp__issuebear__get_task,
mcp__issuebear__add_comment and so on. Use this prefix in your own skills and
prompts. A bootstrap can add more servers. issuebot
merges the servers of the source last, thus a repository cannot replace them.
The harness also reads a run log. issuebot logs asks the configured harness to
read the log. If issuebot cannot find a harness, it shows the raw lines.
By default, each run starts a new agent that reads the comments of the task for
context. On the claude harness, issuebot can keep the session id of each task
and continue that session:
# fragment: one table of a full configuration
[claude]
resume_sessions = trueissuebot writes the session ids to <state dir>/sessions.json with the 0600
permissions. To use a different directory, set the $ISSUEBOT_STATE variable.
If a session is expired, issuebot starts a new agent. Use
issuebot claude session list and issuebot claude session prune to manage
the sessions.
The resume_sessions key controls the local environment. In a Railway sandbox,
issuebot always keeps the session of a paused task and continues it on the next
run. Only claude has sessions, which is currently every harness you can
select — a harness without sessions would restore the worktree in a sandbox
but start a new conversation each run.
issuebot listen shows a live feed in your terminal while a task runs. It also
writes the full transcript to a log for each run, gives a warning if the agent
stops to write, and stops correctly at Ctrl-C.
Each event of the agent is one line: 🔧 for a tool, 💬 for text from the agent, and ✓ for the final result of the run.
▶ ISS-42 — working in /path/to/repo
log: /home/me/.local/state/issuebot/logs/ISS-42-20260629-200000.jsonl
🔧 Read: src/issuebot/runner.py
🔧 Bash: uv run pytest -q
💬 Found the failing case — fixing the off-by-one in the claim loop.
🔧 Edit: src/issuebot/runner.py
✓ Done — posted a summary comment and reassigned for review.
✓ ISS-42 done in 92s
The feed starts with a ▶ line and the path of the log of that run.
It shows the elapsed time, and ends with ✓ or ✗. If you listen on more than
one board, each line starts with the ref of the task. The feed goes to
stderr, thus it does not pollute stdout.
If there is no output for a long time, issuebot gives a warning:
⚠ ISS-42 — no output for 90s (elapsed 240s) — still running; Ctrl-C to abort
issuebot repeats the warning, with new numbers, until the agent writes again.
Ctrl-C stops the agent and releases the run. Thus no claim stays on the board. A second Ctrl-C exits immediately.
The runner sends its warnings to stderr, thus journalctl and docker logs
show them. To show more, set the $ISSUEBOT_LOG_LEVEL variable to DEBUG,
INFO, WARNING, ERROR or CRITICAL. The default is WARNING. An unknown
value keeps WARNING. The dashboard log tail always shows the INFO lines.
ISSUEBOT_LOG_LEVEL=INFO issuebot listenBy default, a run ends only when the agent exits, or when you stop it. To set a maximum time in minutes, use this key:
# fragment: one key of a full configuration
task_timeout_minutes = 30If a run is longer than the maximum, issuebot stops it, releases it, and classifies it as timed out.
issuebot always writes the full output of the harness to this file:
~/.local/state/issuebot/logs/<ref>-<timestamp>.jsonl
issuebot obeys the $XDG_STATE_HOME variable. Use tail -f on the file, or
use these commands:
issuebot logs # the recent runs, the newest first
issuebot logs ISS-42 # the latest run of that ref, as the feed
issuebot logs ISS-42 --raw # the latest run of that ref, as raw lines
issuebot logs ISS-42 -n 100 # the last 100 lines (0 = all the lines)
issuebot logs -f # follow the run that is active now
issuebot logs ISS-42 -f # follow the latest run of that refWith -f and no ref, issuebot follows the run that the runner reports. If
there is no active run, it follows the most recent run on the disk.
issuebot status shows each configured connection. If a listen process runs
on this machine, the command also shows its phase, its task, and the log path
of that run:
$ issuebot status
Runner: active (pid 8421, v0.1.0, updated 3s ago).
myproj board-1 /home/me/code/myproj working ISS-42 /home/me/.local/state/issuebot/logs/ISS-42-20260629-200000.jsonl
docs board-2 /home/me/code/docs waiting —
The command reads a local status file that the listener writes. There is no request to the server, thus the command operates offline and in a different terminal. If the file is absent or old, no runner is active. issuebot shows the connections.
The board selects which skills an agent works a task with, and sends them with
the task. On the claude harness, issuebot fetches and caches whatever the
board selected, then loads it into the agent with --plugin-dir, alongside
your own skills. A task the board sends no skills for runs with none — that is
the board's decision, not a degraded install.
Skill loading is claude-only, which is why Codex is not a selectable
harness (see Harnesses). --plugin-dir is a Claude Code flag
with no Codex equivalent, and the prompt document naming a skill is written
once and shared by every harness — nothing in it is harness-aware, so it
cannot suppress "Use your X skills to do this well" for a harness that
never loaded any. A Codex run would see that sentence with nothing behind it,
for every skill the board sent. Rather than ship that silently, harness = "codex" is refused outright until Codex has its own way to receive the
board's skills.
One of those skills gets a second use. When the github sink asks the harness
to write a pull request description (see Sinks), it carries the
board's writing-pull-requests skill along, already resolved to plain prose,
and the harness weaves it into that read-only call. That call loads no
plugin, so this is the only way the board's guidance reaches it — a task the
board sends no such skill for gets a plain description with none.
The board gives the agent three tools:
- the plan —
set_planrecords what the agent will do. The task holds one plan. The agent changes that plan, and does not write a new comment. Thus the activity log shows the history. The agent always writes a plan. - questions —
ask_questionswrites the questions of the agent on the task as a form. The agent always asks, and does not guess. - confirmation —
request_confirmationasks the person if the agent can continue: Yes or No. If the answer is No, the person writes what to change.
The confirm setting of a connection controls the confirmation:
yes(default) — the agent writes the plan and waits for approval before it writes code.no— the agent writes the plan and continues. The agent can still userequest_confirmationfor an operation that it cannot undo.
The done setting of a connection controls the last step:
review(default) — the agent writes a summary and assigns the task back for review. The agent does not complete the task.complete— the agent writes a summary and completes the task.
A repository can set how issuebot prepares its workspace. To do this, commit a
.issuebear.toml file to the root of the repository. The file is optional.
# fragment: this is .issuebear.toml in a target repository, not the runner configuration
[bootstrap]
# Shell commands that run in sequence in the workspace before the agent starts.
setup = ["uv sync", "npm ci"]
[bootstrap.env] # exported for the setup commands and for the agent
NODE_ENV = "test"
[[bootstrap.mcp]] # more MCP servers, merged below the board MCP
name = "chrome-devtools"
command = "npx"
args = ["-y", "chrome-devtools-mcp@latest"]
[bootstrap.plugins] # more --plugin-dir entries, relative to the repository
dirs = [".claude/plugins/browser"][[bootstrap.mcp]] also accepts an HTTP form: type = "http", url = "…" and
headers = { … }.
Times. The setup commands run when issuebot makes the workspace, and
again after a change to the [bootstrap] table. The env, mcp and plugins
data apply at each start of the agent.
Trust. The setup commands run at the trust level of the agent, in the same workspace and with the same permissions. Use issuebot only with repositories that you trust.
Failure. If the file is incorrect, or if a setup command fails, the task
fails and the agent does not start. If the run cannot report changes,
issuebot uses the folder of the connection and the run continues.
issuebot git worktree list
issuebot git worktree prune <ref>… | --all | --merged [--force]
issuebot git clone list
issuebot git clone prune <ref>… | --all | --merged [--force]
issuebot claude session list
issuebot claude session prune <task-id>… | --all | --completed
issuebot railway prune-checkpoints [--ttl-hours N] [--connection NAME]Each prune command needs a selector. A prune command refuses a workspace
with uncommitted changes or unpushed commits. To delete it, use
issuebot git worktree prune --force. The issuebot git clone prune --merged
command uses gh pr view to find the work that is complete. The
issuebot claude session prune --completed command deletes the entries of
tasks that are complete on the board.
issuebot deletes nothing automatically. The clones and the worktrees stay until you delete them.
- The PAT stays on your machine, in
~/.config/issuebot/config.tomlwith the0600permissions. The server does not read your configuration. Use a dedicated agent PAT that you can revoke, not a personal token. pscan show the PAT during the setup. On a harness that runs Claude Code,issuebot initrunsclaude mcp add --header "Authorization: Bearer <pat>", which puts the PAT in a command-line argument. This is a risk on a machine that more than one person uses.- The agent runs unattended and asks for no approvals. A harness that runs
Claude Code uses
--dangerously-skip-permissions, thus the agent can change files and run commands in its workspace. Use issuebot only with a workspace and a branch that the agent can safely change. - The description agent reads, but does not write. To write a pull request
description, the
claudeharness runs a second, separate agent with a read-only tool list: it can read files and rungit diff,git log,git show,git statusand read-onlyghcommands. It does not get--dangerously-skip-permissions. If a tool is refused, the sink uses the mechanical description instead. - A response is a restriction on the report, not on the tools. Refer to What a run can report. The agent keeps its file tools and shell tools. For full containment, use a sandbox environment for the connection.
- A
.issuebear.tomlfile runs shell commands in the workspace, at the trust level of the agent. It can also add MCP servers. Use issuebot only with repositories that you trust. - Use one agent identity for each machine. A runner uses one agent PAT. Do not share the PAT.
- A session id is a resumption token. With
resume_sessions = true, issuebot writes the session ids tosessions.jsonwith the0600permissions. Delete the file to make the session ids invalid. - Sink credentials stay on the controller. A sink does not run in a sandbox,
thus its credential does not go to a sandbox. A sandbox gets the board URLs
and the agent PAT from your configuration, and the shared variables of the
Railway environment:
GH_TOKENandANTHROPIC_API_KEY.
To work on issuebot, or to write a plugin, refer to
docs/ARCHITECTURE.md.