A self-hosted GitHub PR collaborator bot. Buffalo polls your repos for @buffalo comments on pull requests, clones the relevant branch, runs a CLI agent (Claude Code or Codex CLI) inside a tmux window, commits and pushes the result, and replies on the PR.
Every CLI session runs in its own tmux window. You can attach at any time to watch, take over, or debug. A regex-based command whitelist controls what the agent is allowed to run — anything else gets escalated to humans via a PR comment.
Code review bots that run in opaque containers give you no visibility and no escape hatch. Buffalo runs locally (or on your own server), in tmux, where you can see exactly what's happening and intervene when needed. It's a human-in-the-loop system that happens to automate the boring parts.
curl -fsSL https://raw.githubusercontent.com/cosmicbuffalo/buffalo/main/install.sh | shRequires Node >= 22, tmux, and the GitHub CLI (gh) (authenticated with gh auth login).
Buffalo works best with a dedicated GitHub account that acts as the bot. This keeps bot activity separate from your personal account and makes it obvious which comments and commits came from automation.
- Create a new GitHub account (e.g.
my-buffalo-bot). Use a separate email address. - Generate a Personal Access Token (classic) on the bot account:
- Go to Settings > Developer settings > Personal access tokens > Tokens (classic)
- Create a token with the
reposcope - Fine-grained tokens don't support repos where the bot is an outside collaborator, so use a classic token
- Add the bot as a collaborator on each repo (Settings > Collaborators). It needs write access to push commits and post comments.
- Run
buffalo initand paste the bot account's token when prompted.
The bot account's username is what appears on commits and PR comments. Authorized users (configured during buffalo init) are the human accounts allowed to command the bot — the bot account itself doesn't need to be in that list.
Tip: If you're just trying Buffalo out, you can skip the dedicated account and use your own PAT. You can always switch to a bot account later by updating
~/.buffalo/repos/<owner>/<repo>/config.json.
cd your-repo
buffalo init # walks you through setup
buffalo start # begins polling for @buffalo commentsbuffalo init will ask for:
- A GitHub Personal Access Token (use a dedicated bot account)
- A bot mention tag (default:
@buffalo) - Which GitHub users are authorized to command the bot
- Which CLI backend to use (
claudeorcodex)
Once running, any authorized user can comment @buffalo fix the typo in README.md on a PR and Buffalo will:
- Clone the PR's branch
- Open a tmux window and run the CLI agent with the request
- Auto-approve whitelisted commands, escalate everything else (Codex only — see Backend Differences)
- Commit and push the changes
- Reply on the PR with what it did
buffalo init # Set up a repo (interactive)
buffalo start # Start polling (current repo, or all repos if outside a repo)
buffalo stop # Stop polling
buffalo status # Show all repos, active sessions, pause state, and poller healthCommands auto-detect the current branch via git rev-parse when no branch is specified.
buffalo attach # Attach to tmux window for current branch
buffalo attach feature-branch # Attach to a specific branch's window
buffalo attach --repo owner/repo # Attach to a repo's tmux session
buffalo pause # Pause bot on current branch (CLI keeps running)
buffalo pause feature-branch # Pause a specific branch
buffalo resume # Resume processing
buffalo resume feature-branchbuffalo list # List all active tmux windows across all repos
buffalo logs # Tail pipe-pane log for current branch
buffalo logs feature-branch # Tail a specific branch's log
buffalo history # Show action history for current branch
buffalo history feature-branch # Show action history for a specific branchRecent daemon and repo poll errors are written to:
~/.buffalo/daemon-errors.log
If a repo hits repeated poll failures, Buffalo now retries it with exponential backoff instead of stopping all repos.
Buffalo now posts commit SHAs as GitHub links in PR and issue-related comments.
buffalo whitelist # Show current whitelist (global + repo)
buffalo whitelist add "^docker\b" # Add a regex pattern
buffalo whitelist remove 3 # Remove pattern by indexBuffalo supports two CLI backends, and they behave differently:
| Codex | Claude | |
|---|---|---|
| Mode | Interactive (codex exec) |
Non-interactive (claude --print) |
| Command approval | Whitelist enforced, unapproved commands escalated via PR comment | Not applicable — --print runs non-interactively with no command prompts |
| Session resume | Supported (codex exec resume) |
Not supported (always starts fresh) |
| Pause / resume | Full support (agent keeps running in tmux) | Agent runs to completion, no mid-session pause |
If you choose Claude as your backend, the command whitelist and approval system described below do not apply. Claude runs in single-shot mode and manages its own tool use. The whitelist is only relevant for Codex sessions.
Buffalo enforces a regex-based whitelist on every command the Codex agent wants to run. Compound commands (&&, ||, ;, |, $(), backticks) are split apart and each piece is checked independently.
Default safe patterns (pre-populated on init):
- Read-only:
git status/diff/log/show/branch,ls,cat,head,tail,find,grep,wc,echo - Build/test:
npm test/run/install,node,npx,python,pip install - File ops:
sed,awk,mkdir,touch,cp,mv
When a command isn't whitelisted, Buffalo posts a PR comment:
Command approval needed
Buffalo wants to run:
docker build -t myapp .The part
docker build -t myapp .is not in the whitelist.Reply with:
@buffalo allow once— approve this one time@buffalo allow always ^docker\b— add a regex pattern to the whitelist@buffalo deny— reject this command
Buffalo polls GitHub for new comments every 15 minutes by default (configurable via pollIntervalMs). Only comments from authorized users that mention the bot tag are processed. It also checks whether tracked PRs have been merged or closed, and cleans up accordingly.
For faster response times, lower pollIntervalMs in your config (e.g. 60000 for 1 minute). Keep in mind that GitHub's API rate limit is 5,000 requests/hour for authenticated users — each poll cycle makes several API calls per repo, so very aggressive intervals with many repos could hit limits.
Note: Buffalo is purely polling-based. There is no webhook support, so there will always be some latency between a comment being posted and Buffalo acting on it.
One tmux session per repo, one window per branch:
tmux session: "buffalo-owner-repo"
├── window: "feature-branch" ← running claude CLI
├── window: "fix-bug" ← running codex CLI
└── window: "refactor-api" ← completed, output captured
Output is captured via tmux pipe-pane to log files that Buffalo monitors for approval prompts and completion signals.
buffalo pause sets a flag — Buffalo stops reading the log for that window, but the CLI keeps running in tmux. You can attach, interact freely, then buffalo resume to hand control back. This is useful when the agent gets stuck and you want to manually fix something before letting it continue.
Multiple @buffalo comments on the same PR within a poll cycle are batched into a single prompt. The CLI agent sees all requests at once and addresses them together.
~/.buffalo/
config.json # Global config
whitelist.json # Global command whitelist
repos/
owner/
repo/
config.json # Per-repo overrides
whitelist.json # Per-repo whitelist additions
sessions.json # Active session state
last_poll.json # Last poll timestamp
workspaces/
feature-branch/ # Cloned branch worktree
logs/
feature-branch.log # Pipe-pane output
history/
feature-branch.jsonl # Action audit trail
Every action Buffalo takes is logged as a JSONL event in history/<branch>.jsonl:
{"ts":"2026-02-25T10:15:00Z","type":"comment_detected","pr":123,"author":"user1","body":"@buffalo fix the typo"}
{"ts":"2026-02-25T10:15:05Z","type":"cli_started","pr":123,"command":"claude --print ..."}
{"ts":"2026-02-25T10:16:30Z","type":"command_requested","pr":123,"command":"sed -i 's/teh/the/' README.md","approved":true}
{"ts":"2026-02-25T10:17:00Z","type":"commit_pushed","pr":123,"sha":"abc123"}
{"ts":"2026-02-25T10:17:05Z","type":"comment_posted","pr":123}
{"ts":"2026-02-25T12:00:00Z","type":"pr_merged","pr":123}
{"ts":"2026-02-25T12:00:01Z","type":"cleanup","pr":123,"tmux_window_destroyed":true}History files persist after cleanup so you always have an audit trail.
{
"githubToken": "ghp_...",
"authorizedUsers": ["alice", "bob"],
"defaultBackend": "claude",
"pollIntervalMs": 900000
}{
"botUsername": "my-buffalo-bot",
"authorizedUsers": ["alice"],
"backend": "codex",
"pollIntervalMs": 300000,
"githubToken": "ghp_repo_specific_token"
}Per-repo config overrides global. Whitelists are merged (global + repo).
git clone <repo-url>
cd buffalo
npm install
npm run build
npm test # runs all testsTests use Node's built-in test runner, temp directories for filesystem isolation, real tmux sessions for integration tests, and local bare git repos for clone/push verification. No mocking frameworks — just the standard library.
MIT