Pick the next ClickUp ticket and start a Claude Code session on it. One command from terminal to PR.
Project description
clickup-work
A tiny CLI that picks the next ClickUp ticket assigned to you, cuts the right branch in the right repo, and launches a Claude Code session with the ticket pre-loaded. On exit, it opens a GitHub PR.
One command, from terminal to solving the problem.
Quick start
Four commands from zero to running:
# 1. Install
pipx install clickup-work
# 2. Save your ClickUp API token once (generate at ClickUp → Settings → Apps → API Token)
clickup-work login
# 3. Register the repo you want to work in
clickup-work add-repo ~/projects/my-app
# 4. Go
clickup-work --repo my-app
The token is stored in ~/.config/clickup-work/config.toml with mode 0600,
so any new shell can use it without exporting an env var. Full details on
each step are below.
What it does
$ clickup-work --repo mobile
clickup-work · pick a ticket — 4 open
┌──────────────────────────────────────────────────────────────────┐
│ type to filter · / to refocus · esc to clear │
└──────────────────────────────────────────────────────────────────┘
── Mobile · 1 ──
urgent to do iOS launch crash Bugs
── Product · 2 ──
urgent in progress Fix flaky checkout tests Sprint 12
high to do Add dark-mode toggle Sprint 12
── Billing · 1 ──
normal to do Refactor notification queue Infra
4 / 4 tickets visible · ↑↓ nav · enter pick · / filter · q quit
(Enter on iOS launch crash)
clickup-work · ready to cut — 86c9abc
┌──────────────────────────────────────────────────────────────────┐
│ iOS launch crash │
│ 86c9abc · urgent priority · to do │
│ list: Engineering / Mobile / Bugs │
│ url: https://app.clickup.com/t/86c9abc │
│ │
│ repo: /home/you/projects/mobile-app (nickname: mobile) │
│ branch: fix/ios-launch-crash → PR into main │
└──────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────┐
│ base branch (from origin/HEAD) — edit to override: │
│ main │
└──────────────────────────────────────────────────────────────────┘
(Enter — branch is cut, Claude Code launches in the same terminal)
…work with Claude, commit, exit…
clickup-work · post-Claude — 86c9abc
┌──────────────────────────────────────────────────────────────────┐
│ iOS launch crash · 86c9abc │
│ │
│ branch: fix/ios-launch-crash │
│ base: main │
│ commits: 2 ahead │
│ │
│ Press Y to push and open the PR. Press N to leave it local. │
└──────────────────────────────────────────────────────────────────┘
[ Push & open PR ] [ Skip — branch stays local ]
(Y — push runs, then status / time / reassign modals chain)
→ pushing fix/ios-launch-crash and opening PR…
✓ PR opened: https://github.com/you/mobile-app/pull/42
✓ ticket moved to in review
✓ logged 1h 30m of time
✓ also assigned huzaifa
all done. press q to exit.
Why
Because switching context between ClickUp, your terminal, your git branches, and your editor is the slowest part of shipping a ticket. This automates the mechanical parts and hands control to Claude Code for the actual work.
- Picks the right ticket via a Textual-powered TUI (filterable list, color-
coded priority, keyboard navigation) — or
--topto auto-pick - Confirms the cut on a plan screen (ticket card + base-branch input)
- Cuts a conventional branch (
feat/<slug>,fix/<slug>,docs/<slug>, inferred from ClickUp task type — or override with--prefix) - Never guesses the base branch (explicit per-repo config,
origin/HEADfallback, verified against origin before any git operation) - After Claude exits, opens a TUI for push confirmation, status update, time tracking, and reassignment — all in one session
- Opens the PR via
ghonly when there are real commits — no noise, no force-push - Falls back to plain text via
--no-tuifor pipes, CI, or fzf preference
Requirements
- Python 3.11+
claude— Claude Code CLIgitandgh, withgh auth logindone- A ClickUp API token (generate one under Settings → Apps → API Token)
- A 256-color, Unicode-capable terminal for the TUI (any modern terminal qualifies)
fzf(optional — only used when you pass--no-tui; otherwise unused)
textual (the Python TUI library) is pulled in automatically by pipx.
Install
With pipx (recommended)
pipx install clickup-work
Don't have pipx? pip install --user clickup-work also works on most systems.
On Arch/Debian-managed Pythons (PEP 668), use pipx or a venv.
Install the latest dev version directly from GitHub
pipx install git+https://github.com/Azhar-ud/clickup-work.git
From source
git clone https://github.com/Azhar-ud/clickup-work.git
cd clickup-work
python -m venv .venv && source .venv/bin/activate
pip install -e .
clickup-work is now on your PATH.
Configure
1. Token
The recommended one-time setup:
clickup-work login
# Paste your ClickUp API token (input hidden): ********
# ✓ token valid (ClickUp user id 12345)
# ✓ token saved to ~/.config/clickup-work/config.toml (mode 0600 — readable only by you).
The token lives in ~/.config/clickup-work/config.toml, so any new shell
can use it — no export … line in ~/.zshrc, no missing-token errors
when you open a new terminal.
The env var path still works for CI / scripts and overrides the saved token when both are present:
export CLICKUP_API_TOKEN=pk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
2. Config file
Copy the example and edit:
mkdir -p ~/.config/clickup-work
cp config.toml.example ~/.config/clickup-work/config.toml
$EDITOR ~/.config/clickup-work/config.toml
The quickest way to add a repo is the built-in helper — no TOML syntax to remember:
clickup-work add-repo ~/projects/my-app
# Found repo at /home/you/projects/my-app
# Detected default branch: main
# Nickname for this repo [my-app]: my-app
# Added [repos.my-app] to ~/.config/clickup-work/config.toml
Or hand-write a block:
[repos.my-app]
path = "/home/you/projects/my-app"
base_branch = "main"
# branch_prefix = "feat" # optional override
See config.toml.example for all fields.
Usage
# Pick a ticket from the fzf picker, open it in the named repo
clickup-work --repo my-app
# Skip the picker; take the top-priority ticket
clickup-work --repo my-app --top
# One-off overrides
clickup-work --repo my-app --base staging # target a different base
clickup-work --repo my-app --prefix fix # force fix/… prefix
clickup-work --repo my-app --draft # open PR as a draft
# Preview only — no git, no Claude
clickup-work --repo my-app --dry-run
# See every API call and git command
clickup-work --repo my-app --verbose
# Register a new repo in config
clickup-work add-repo ~/projects/new-repo [--name nickname] [--base-branch main]
# Personal workload report (this week + next week)
clickup-work workload # show your load
clickup-work workload set-capacity 4 # save 4h/day to config
# Persist a TUI theme (ben10 = Omnitrix-green palette + binary banner)
clickup-work theme # show current + available
clickup-work theme ben10 # persist for every run
clickup-work theme default # clear the preference
# Or pick visually inside any TUI surface — Ctrl+P opens Textual's command
# palette; pick "Change theme" → ben10 (or any of the 20+ built-in themes
# Textual ships). The choice persists to config.toml automatically.
Full flag list
| Flag | Purpose |
|---|---|
--repo NAME_OR_PATH |
Repo nickname from config, or an absolute/~ path |
--base BRANCH |
Override base branch for this run |
--prefix NAME |
Override branch prefix (feat, fix, chore, docs, …) |
--top, -t |
Auto-pick top-priority ticket (skip picker) |
--draft |
Open the resulting PR as a draft |
--no-status |
Skip the "move ticket to which status?" prompt after the PR opens |
--no-time |
Skip the "track time spent / update estimate?" prompts after the PR opens |
--no-assign |
Skip the "reassign to which member?" prompt after the PR opens |
--yes, -y |
Skip the "push branch and open PR?" confirmation prompt |
--no-tui |
Drop to the plain-text flow (fzf or numbered picker, bare prompts) |
--theme NAME |
Visual theme for the TUI: default (textual-dark) or ben10 (Omnitrix-green palette + scrolling-binary banner on the picker). Also reads $CLICKUP_WORK_THEME and theme = "..." in config.toml (set with clickup-work theme ben10). Precedence: flag > env > config. |
--dry-run |
Preview the ticket + plan, touch nothing |
--verbose, -v |
Print every HTTP request and shell command |
Interactive UI
Everything happens in a Textual TUI when stdout is a terminal. Three surfaces in the main flow, plus the workload report:
| Surface | Triggered by | What it does |
|---|---|---|
| Ticket picker | clickup-work [--repo X] |
Filterable list of your open tickets, grouped by folder. Type to filter (matches name + status + folder + tags), arrows to navigate, Enter to pick (sends to Claude), a to open the actions modal. |
| Ticket actions | Press a on a ticket in the picker |
View the ticket's description and metadata. Mutations: s status, p priority, d due date, b start date, e set estimate, t log time, r rename, D description (opens $EDITOR), T toggle tag, A assignees (add/remove), S subtasks (list/create/status), c view/post comments, H time-entry history (edit/delete past entries). Plus o open in browser, g send to Claude, q back. Date inputs accept 2026-05-10, +3d, today, tomorrow, or clear. |
| Plan screen | After a ticket is picked | Shows the ticket card and the resolved branch / base. Edit the base inline if you want to target staging, dev, etc. Enter to launch Claude. |
| Post-Claude flow | After Claude exits with commits | Push & open PR (Y/N), then a chain: status picker → time spent → time estimate → reassign. Each step skippable with Esc. |
| Workload | clickup-work workload |
This week + next week capacity bars, missing-estimates report, inline e to set estimate / s to change status. |
Universal keybinds in any TUI surface:
| key | does |
|---|---|
q |
quit / cancel |
Esc |
clear filter, dismiss modal, or quit |
↑ / ↓ |
navigate the current list |
enter |
confirm / pick / submit |
/ |
focus the filter input where applicable |
Falling back to plain text — pass --no-tui (or pipe / redirect
stdout) for the original terminal flow. The fzf-driven picker is still
available there if fzf is installed.
How it picks the repo (multi-project workflow)
When you're assigned tickets across several ClickUp folders that map to different repos, the tool can route each ticket to the right repo automatically. You don't set anything up upfront — it learns as you go.
First time you pick a ticket from a folder it hasn't seen:
Ticket "iOS launch crash" is in folder "Mobile",
which isn't linked to any repo yet.
Which repo should this folder route to?
1. marketing (/home/you/marketing-site)
2. billing (/home/you/billing-service)
3. mobile (/home/you/mobile-app)
(or q to cancel and pass --repo manually)
> 3
✓ folder "Mobile" now routes to 'mobile' (saved to ~/.config/clickup-work/config.toml)
The mapping is saved as folder_ids = ["<id>"] inside the repo's config
block. Future tickets from that folder skip the prompt and go straight
to the right repo.
Resolution order when you run clickup-work:
--repo <name>→ always wins; scopes the picker to that repo's foldersdefault_repoin config → backward-compatible single-repo fallback- Exactly one repo registered → that one
- Otherwise → fetch all assigned tickets, pick one, route by folder
Scoping the picker to one project (focus mode):
clickup-work --repo mobile # only shows tickets from folders linked to 'mobile'
Hand-editing config still works if you prefer:
[repos.mobile]
path = "/home/you/mobile-app"
base_branch = "main"
folder_ids = ["901234567", "901234890"] # optional; tool fills these in
How it picks the base branch
Resolution order for the suggested default, first non-empty wins:
--base <branch>flag[repos.<name>].base_branchin configgit symbolic-ref refs/remotes/origin/HEAD(auto-detect)- Error out — the tool refuses to guess
Unless --base was passed, the tool then prompts to confirm or override:
base branch [main]: _
Press Enter to accept the suggestion or type a different branch name —
useful when a repo has a dev / staging / release line you sometimes
branch off instead of main. Passing --base <branch> skips the prompt.
Before any branching, it verifies the chosen base exists on origin:
git ls-remote --exit-code --heads origin <base>
If it doesn't, the tool aborts with a clear message — no stray branches, no PRs targeting a dead base, no silent typos.
How it picks the branch prefix
| Condition | Prefix |
|---|---|
--prefix <name> |
Whatever you pass |
[repos.<name>].branch_prefix in config |
That value |
| ClickUp task type contains "bug" / is incident/hotfix | fix |
| ClickUp task type contains "doc" | docs |
| Anything else | feat |
Post-session behavior
When you exit Claude:
ahead = git rev-list --count origin/<base>..HEAD
ahead == 0→ print "no new commits on the branch — skipping PR", exit 0ahead ≥ 1→ askpush branch and open PR? [Y/n]; on yes,git push -u origin <branch>thengh pr create --base <base> --head <branch>. Pass--yes/-yto skip the prompt.--draftpassed → PR is opened as a draft
If you answer n at the confirmation, the feature branch stays local —
you can push it by hand whenever you're ready. No force-push, no reset.
Once the PR is open, the tool prompts you to move the ClickUp ticket to a
new status — pulled live from the ticket's list, so whatever your workspace
is configured to use (in review, qa, blocked, …) is what you'll see.
Pick one to update, or hit q / Esc to leave it where it is. Pass
--no-status to skip the prompt entirely.
After the status prompt, two short follow-ups offer to log time spent and
update the ticket's time estimate. Both accept formats like 1h 30m,
90m, or 1.5h (a bare number is treated as minutes). Hit Enter on either
to skip just that one, or pass --no-time to skip both.
A final prompt offers to reassign the ticket to another workspace member
— useful for "I'm done, please review" handoffs. With fzf installed,
you get fuzzy search across name and email (start typing huzaifa and
the list narrows as you type). After picking, a follow-up asks whether
to remove yourself from the ticket too: No keeps you as a co-assignee,
Yes is a clean handoff. Esc or empty pick skips entirely; --no-assign
turns the prompt off.
The PR body itself is generated from the commits on the branch — a
## Summary section bullets each commit subject, a ## Test plan
checklist is left for you to fill in, and the original ClickUp ticket
description is tucked into a collapsed <details> block for reviewer
context.
Workload (personal load report)
If your week is part-time, scattered, or being asked about by a PM, the
workload subcommand answers "what's on your plate?" in one shot —
without opening the ClickUp UI.
By default, when stdout is a terminal, it launches a navigable TUI
(arrow keys to move, e to set an estimate inline, s to change status,
enter to open the ticket in your browser, r to refresh, q to quit).
When stdout is piped or redirected — or you pass --no-tui — it falls
back to a plain-text report:
$ clickup-work workload
Capacity: 4h/day · 20h/week
This week (May 4 – May 10):
████████████████░░░░ 16.5h / 20h ✓ under
• 86c9abc Fix auth bug 4h due Wed
• 86c9def Refactor cache layer 8h due Fri
• 86c9ghi Review PR #88 2.5h due Thu
• 86c9jkl Old overdue ticket 2h OVERDUE (2026-05-01)
Next week (May 11 – May 17):
████████████████████ 26h / 20h ⚠ OVER by 6h
• 86c9mno Migrate session store 16h due 2026-05-12
• 86c9pqr Spike: new search backend 10h due 2026-05-15
⚠ 2 assigned ticket(s) have no time estimate — Workload can't see them:
• 86c9stu Investigate flaky test
• 86c9vwx Update docs
Tickets without a due date (not bucketed): 1
• 86c9yza Onboarding doc 4h
Setting your capacity
clickup-work workload set-capacity 4 # 4h/day, weekly capacity = 20h
clickup-work workload set-capacity 4h # same — `h` suffix accepted
clickup-work workload set-capacity 4.5 # decimals fine
This writes a [workload] block to your config:
[workload]
hours_per_day = 4
The default if absent is 8 (full-time). Weekly capacity is always
hours_per_day × 5 weekdays.
Flags
| Flag | Purpose |
|---|---|
--hours-per-day HOURS |
Override capacity for this run only — does not write config |
--no-tui |
Force the plain-text report even when stdout is a TTY |
--no-unestimated |
(plain mode only) Hide the "tickets without a time estimate" section |
How tickets land in each section
| Ticket has… | Lands in… |
|---|---|
due_date in this week (or overdue) and a positive time_estimate |
This week's bar + ticket list |
due_date in next week and a positive time_estimate |
Next week's bar + ticket list |
due_date in either week but no estimate |
Unestimated section (Workload view is blind to these) |
No due_date |
Undated section |
due_date more than two weeks out |
Out of horizon — not shown |
The bar is hours_used / weekly_capacity; it fills up as estimates pile
on. The header underneath tells you whether you're under, at, or over —
and by how many hours.
Why a separate [workload] block
ClickUp's Workload view configures capacity per-user inside the ClickUp
UI, but that capacity setting is not exposed through the public
REST API. There's no endpoint to read it. So clickup-work keeps its
own copy in your local config — set-capacity writes it, the report
reads it. If you want the same number in ClickUp's Workload view too,
set it there once via the Workload settings panel.
Safety
- Plaintext of your token never leaves
CLICKUP_API_TOKEN(env) or your shell rc. Nothing is written back to disk by this tool. - The tool never does
git reset --hard, force-pushes, or amends. - If a feature branch already exists, it's reused (not reset) — good for resuming partial work.
--dry-runruns everything up to "touch disk", including the branch-exists-on-origin check, and stops there.
License
MIT. See LICENSE.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file clickup_work-0.20.0.tar.gz.
File metadata
- Download URL: clickup_work-0.20.0.tar.gz
- Upload date:
- Size: 76.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4581b7a36ed5f993a3aace58bad1e1e5600fa2a3b1df1b6d0c096e48f14781f9
|
|
| MD5 |
98ae1972027bb32676abb89d088e153b
|
|
| BLAKE2b-256 |
89d8a929b38e611ea87f2ba7f74b863670edd1042e2e6bfa6aa39758bbed07b1
|
Provenance
The following attestation bundles were made for clickup_work-0.20.0.tar.gz:
Publisher:
publish.yml on Azhar-ud/clickup-work
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
clickup_work-0.20.0.tar.gz -
Subject digest:
4581b7a36ed5f993a3aace58bad1e1e5600fa2a3b1df1b6d0c096e48f14781f9 - Sigstore transparency entry: 1419808501
- Sigstore integration time:
-
Permalink:
Azhar-ud/clickup-work@0d1e31380495931028ef60c977f99d7f9da31632 -
Branch / Tag:
refs/tags/v0.20.0 - Owner: https://github.com/Azhar-ud
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@0d1e31380495931028ef60c977f99d7f9da31632 -
Trigger Event:
push
-
Statement type:
File details
Details for the file clickup_work-0.20.0-py3-none-any.whl.
File metadata
- Download URL: clickup_work-0.20.0-py3-none-any.whl
- Upload date:
- Size: 76.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
13692f5307464bbbbf114d2eee53953a2e78b998e5e6846dce744a0db7ad1c76
|
|
| MD5 |
cd7e3398d8bf315528fff22d05b41683
|
|
| BLAKE2b-256 |
dccabb4c70f2872a469418370888185292b3db2f57adb85ec3f066f8547ed7a7
|
Provenance
The following attestation bundles were made for clickup_work-0.20.0-py3-none-any.whl:
Publisher:
publish.yml on Azhar-ud/clickup-work
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
clickup_work-0.20.0-py3-none-any.whl -
Subject digest:
13692f5307464bbbbf114d2eee53953a2e78b998e5e6846dce744a0db7ad1c76 - Sigstore transparency entry: 1419808585
- Sigstore integration time:
-
Permalink:
Azhar-ud/clickup-work@0d1e31380495931028ef60c977f99d7f9da31632 -
Branch / Tag:
refs/tags/v0.20.0 - Owner: https://github.com/Azhar-ud
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@0d1e31380495931028ef60c977f99d7f9da31632 -
Trigger Event:
push
-
Statement type: