Claude Skill

basecamp

Interact with Basecamp via the Basecamp CLI. Full API coverage: projects, todos, cards, messages, files, schedule, check-ins, timeline, recordings, templates, webhooks, subscriptions, lineup, chat, pings, gauges, assignments, notifications, bookmarks, bubble-up, drafts, notes, ca

LLM Mart · 0 points · 2 views 13 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download basecamp-basecamp-cli-skills_basecamp-3403eeb.zip · 27 KB
Part of basecamp/basecamp-cli — 2 skills

Install

skills CLI npx skills add https://github.com/basecamp/basecamp-cli/tree/main/skills/basecamp
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install basecamp-basecamp-cli@llmmart
Git git clone https://github.com/basecamp/basecamp-cli.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole basecamp/basecamp-cli collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

/basecamp - Basecamp Workflow Command

Full CLI coverage: 195 tracked in-scope endpoints across todos, cards, messages, files, schedule, check-ins, timeline, recordings, templates, webhooks, subscriptions, lineup, chat, pings, gauges, assignments, notifications, the account event feed, and accounts.

Agent Invariants

MUST follow these rules:

  1. Choose the right output mode — --jq when you need to filter/extract data; --json for full JSON; --md when presenting results to a human (see Output Modes below). Never pipe to external jq — use --jq instead.

  2. Parse URLs first with basecamp url parse "<url>" to extract IDs

  3. Comments are flat - reply to parent recording, not to comments

  4. Check context via .basecamp/config.json before assuming project

  5. Content fields accept Markdown, and most accept @mentions — the CLI converts these rich-text fields from Markdown to HTML: message bodies, document bodies, comment content, todo descriptions, card bodies, schedule entry descriptions, upload descriptions, check-in answers and notes. Two rich-text fields are sent as written, so give them HTML: todolist descriptions and gauge needle descriptions. Chat is different again: chat post sends plain text unless you pass --content-type text/html or the line carries a mention. Use Markdown formatting (lists, bold, links, code blocks, tables) for rich content. @mentions resolve in message bodies, comment content, card bodies, schedule descriptions and chat lines — not in todo descriptions, documents, uploads, check-ins or notes. Four mention syntaxes are available (prefer deterministic for agents):

    • [@Name](mention:SGID) — zero API calls, embeds SGID directly (preferred for agents)
    • [@Name](person:ID) — one API call, resolves person ID to SGID via pingable set
    • @sgid:VALUE — inline SGID embed for pipeline composability
    • @Name / @First.Last — fuzzy name resolution (may be ambiguous)

    Raw HTML is also accepted, but it is all-or-nothing per field: a tag the CLI detects as HTML (<p>, <ul>, <strong>, <a>, <img>, <table> and the other common formatting tags) outside a backtick code span or backtick fence (a ~~~ fence does not hide it) skips Markdown conversion for the whole field, so any Markdown alongside it — ![alt](/local/path) included — is sent literally. The HTML itself goes through as written, except that an empty separator paragraph is inserted between directly adjacent <p> blocks so they render with spacing; a local path in a raw <img src> is still uploaded and replaced with an attachment in every converted field except notes, which take no attachments. Titles (a todo's content argument, card and message titles) are plain text and never converted.

    Table boundary: GFM tables round-trip: they render in message/comment bodies, display converts them back to pipe tables, and the TUI in-place editors open simple grids for editing. Only complex tables — merged cells (colspan/rowspan), captions, extra header rows, nested tables, attachments/images or block content inside cells, multi-paragraph or multi-line cells, or a table inside a blockquote or list — refuse to open, since a GFM pipe table can't represent those shapes (edit them on Basecamp web, or replace the whole field via messages update / comments update / todos update --description, which take fresh content and are unaffected). Complex tables still display best-effort, flattened to a plain grid.

    Multiline / non-ASCII content: do not rely on bash ANSI-C quoting ($'...\n...') — it is a bash/zsh extension. Under a POSIX /bin/sh (dash, busybox-ash, common in sandboxes) the $ is passed through literally and posts a stray leading $, and \n stays a literal backslash-n. Pipe the content via stdin instead, using - as the content argument:

    printf '%s\n' '海报 mockup 方向稿:' '' '<bc-attachment ...>' | basecamp comments create <recording_id> - --in <project> --json
    

    - means "read from stdin" on every content input: content-kind positionals (comments create/update, messages create [body], cards create [body], todos create, docs documents create [content], chat post/update, boost create, checkins answer create/update, notes set) and content flags (--data on api post/put, --body, --content, --description, --comment on todos sweep, --file on notes set). Each command's --agent help lists its stdin inputs. Rules:

    • A pipe is never consumed implicitly — without - it is ignored (or, where content is required and missing, the error teaches -).
    • Only one input can read stdin per invocation.
    • A literal - anywhere else (a title, a name, a path) errors when stdin is piped. Escape a positional after the -- separator (basecamp projects create -- -); a flag value has no in-line escape — run the command without piped stdin. basecamp help and shell completion are exempt: they write nothing to Basecamp, and completion legitimately receives - as the word being completed.
    • - with nothing piped (interactive TTY) errors immediately instead of hanging; use a pipe, a heredoc (basecamp comments create <id> - <<'EOF'), or --edit where offered.
    • Trailing newlines are trimmed from stdin content, so printf 'x\n' | ... - posts x (this keeps boost create - inside its 16-rune limit).
  • Universal - support (and the stray-- guard) shipped in v0.10.0. Older CLIs do not support it consistently: comments create/update read stdin, while unsupported inputs may treat - as literal content or fail. For example, messages create "Title" - posts a body of -, which Markdown renders as an empty bullet list. When the CLI version is unknown, check basecamp --version first, or pass the content portably as "$(cat file.md)" and verify the posted content when it matters.
  1. Project scope is mandatory for most commands — via --in <project> or .basecamp/config.json. Cross-project exceptions: basecamp reports assigned for assigned work, basecamp assignments for structured assignment views, basecamp reports overdue for overdue todos, basecamp reports schedule for upcoming schedule across all projects, basecamp recordings <type> for browsing by type, basecamp notifications for notifications, basecamp gauges list for account-wide gauges, basecamp events poll and basecamp inbox for the account event feed, and the seven list commands covered in item 7.
  2. Account-wide listing. basecamp todos list --all-projects --json lists across every project; the same flag does the same on cards list, messages list, comments list, files list, forwards list, and checkins answers. It overrides a configured project, and with no project in scope those commands already list account-wide rather than prompting. Flags that name something inside a single project are rejected there rather than silently ignored. Account-wide listings return the first 100 items by default — account-wide "all" is the whole account, not one project's worth. Use --limit N to raise the cap (it walks pages until N are collected) or --all for everything. --page N fetches exactly one page, but only on the paginated listings. The two overdue variants — basecamp todos list --all-projects --overdue and basecamp cards list --all-projects --overdue — come from unpaginated endpoints. They accept --limit and --all but reject --page, so do not generate --page against them.

Output Modes

Choosing a mode:

Goal Flag Format
Filter/extract JSON data --jq '<expr>' Built-in jq filter (no external jq needed). Implies --json; filter runs on the envelope.
Filter in agent mode --agent --jq '<expr>' Filter runs on data-only payload (no envelope), matching --agent contract.
Full JSON output --json JSON envelope: {ok, data, summary, breadcrumbs, meta}; errors: {ok:false, error, code, retryable, hint, meta}
Show results to a user --md / -m GFM tables, task lists, structured Markdown
Automation / scripting --agent Success: raw JSON data (no envelope); errors: {ok:false,...} object; no interactive prompts

Always pass --json or --md explicitly — auto-detection depends on config and may not produce the format you expect. Use --md when composing reports, summarizing data, or displaying results inline. --agent is for headless integration scripts.

Avoiding interactive prompts. The flags --agent/--json/--quiet/--ids-only/--count and the environment variable BASECAMP_NONINTERACTIVE=1 suppress interactive selection prompts. --md does not — if a required target is ambiguous (e.g. a project with multiple todosets and no --todoset), and the CLI is attached to a terminal, it will show a blocking picker. When you need Markdown output and no prompts, either pass the flag that names whatever is ambiguous (--todoset <id> for the todoset case above, or --in <project> / --list <id> when the project or list is ambiguous) or set BASECAMP_NONINTERACTIVE=1 in the environment. BASECAMP_NONINTERACTIVE disables all prompts (they become actionable errors instead) without changing the output format — an escape hatch for agents running under a PTY.

Other modes: --quiet (success: raw JSON, no envelope; errors: {ok:false,...}), --ids-only, --count, --stats (session statistics), --styled (force ANSI), -v / -vv (verbose/trace), --jq '<expr>' (built-in jq filter — see below).

CLI Introspection

Navigate unfamiliar commands with --agent --help — returns structured JSON describing any command:

basecamp todos --agent --help
{"command":"todos","path":"basecamp todos","short":"...","long":"...","usage":"...","notes":["..."],
 "subcommands":[{"name":"sweep","short":"...","path":"basecamp todos sweep"}],
 "flags":[{"name":"assignee","type":"string","default":"","usage":"..."}],
 "inherited_flags":[{"name":"json","shorthand":"j","type":"bool","default":"false","usage":"..."}]}

Walk the tree: start at basecamp --agent --help for top-level commands, then drill into any subcommand. Commands carry domain-specific agent hints (e.g., "--assignee filters the account-wide listing only; within a project, fetch all and filter client-side").

Note: a subcommand's inherited_flags is deliberately short — the CLI curates it down to --account, --json, --md, --project, --quiet (and drops --project where the command takes <id|url>). Every other global flag (--jq, --agent, --styled, --verbose, --profile, ...) is listed only at the root (basecamp --agent --help) but still applies on every subcommand (a command that cannot honor one refuses it with an explicit error — version rejects --jq); its absence from a subcommand's help does not mean it is unsupported.

Pagination

basecamp <cmd> --limit 50   # Cap results (default varies by resource)
basecamp <cmd> --all        # Fetch all (may be slow for large datasets)
basecamp <cmd> --page 1     # First page only, no auto-pagination

--all and --limit are mutually exclusive. --page cannot combine with either.

Smart Defaults

  • --assignee me resolves to current user
  • --due tomorrow / --due +3 / --due "next week" — natural date parsing, when setting a due date (todos create, todos update, cards create, and so on)
  • --due on a listing is a different flag and does not take dates: it accepts only with, without, or overdue, and only account-wide. basecamp todos list --due tomorrow is rejected. For date-based listing use --overdue, --no-due-date, or basecamp assignments due <scope>
  • Project from .basecamp/config.json if --in not specified
  • Multiple identities use named profiles: basecamp profile create <name>, then select one with global --profile <name> or BASECAMP_PROFILE=<name>.

Quick Reference

Note: Most queries require project scope (via --in <project> or .basecamp/config.json). Cross-project exceptions: basecamp reports assigned, basecamp assignments, basecamp reports overdue, basecamp reports schedule, basecamp recordings <type>, basecamp notifications, basecamp gauges list, basecamp events poll, basecamp inbox.

Seven list commands also list account-wide: basecamp todos list --all-projects --json, and likewise cards list, messages list, comments list, files list, forwards list, and checkins answers.

Task Command
List projects basecamp projects list --json
My todos (in project) basecamp todos list --assignee me --in <project> --json
My todos (cross-project) basecamp reports assigned --json (defaults to "me")
My schedule (cross-project) basecamp reports schedule --json (upcoming events across all projects)
All todos (cross-project) basecamp todos list --all-projects --json (grouped by project)
Overdue todos (in project) basecamp todos list --overdue --in <project> --json
Overdue todos (cross-project) basecamp todos list --all-projects --overdue --json (flat, oldest first) or basecamp reports overdue --json (bucketed by lateness)
All cards (cross-project) basecamp cards list --all-projects --json (grouped by project)
Someone's todos (cross-project) basecamp todos list --all-projects --assignee "Ann" --json (server-side filter)
Two people's todos (cross-project) basecamp todos list --all-projects --assignee ann --assignee bob --json (matches either)
Someone's cards (cross-project) basecamp cards list --all-projects --assignee "Ann" --json
Todos with no due date set (cross-project) basecamp todos list --all-projects --due without --json
My bookmarks basecamp bookmarks list --json
Bookmark something basecamp bookmarks add <id-or-url> --json
Is it bookmarked? basecamp bookmarks check <id-or-url> --json (always exits 0)
Bubble a recording up basecamp bubble-up add <id-or-url> --json
Schedule a bubble-up basecamp bubble-up add <id-or-url> --at tomorrow --json
Pop a bubble-up basecamp bubble-up remove <id-or-url> --json
My unpublished drafts basecamp drafts list --json
Read my personal note basecamp notes show --json
Replace my personal note basecamp notes set "<content>" --json
Check-ins I owe answers to basecamp checkins reminders --json
Add to Up Next basecamp assignments prioritize <id> --json
Recolor a calendar basecamp calendars update <id-or-url> --color blue --json
Todo outside any list basecamp todos create "<content>" --loose --in <project> --json
Assign todo basecamp assign <id> [id...] --to <person> --in <project> --json
Assign card basecamp assign <id> [id...] --card --to <person> --in <project> --json
Assign card step basecamp assign <id> [id...] --step --to <person> --in <project> --json
Create todo basecamp todos create "Task" --in <project> --list <list> --json
Create todolist basecamp todolists create "Name" --in <project> --json
Complete todo basecamp todos complete <id> --json
List cards basecamp cards list --in <project> --json
Create card basecamp cards create "Title" --in <project> --json
Complete card basecamp cards done <id|url> --in <project> --json
Move card basecamp cards move <id> --to <column> [--position N] --in <project> --json
Move card to on-hold basecamp cards move <id> --on-hold --in <project> --json
Move card to another project basecamp cards move <id> --to-wormhole <wormhole_id> --in <project> --json (async teleport)
Post message basecamp messages create "Title" "Body" --in <project> --json
Post with @mention basecamp messages create "Title" "Hey @First.Last, ..." --in <project> --json
Post silently basecamp messages create "Title" "Body" --no-subscribe --in <project> --json
Post to chat basecamp chat post "Message" --in <project> --json
List pings basecamp notifications --json --jq '.data.reads[]? | select(.section == "pings")'
Read ping thread basecamp api get "/buckets/<circle_id>/chats/<chat_id>/lines.json" --agent
Post to ping thread basecamp api post "/buckets/<circle_id>/chats/<chat_id>/lines.json" --data '{"content":"<p>message</p>"}' --json
Add comment basecamp comments create <recording_id> "Text" --in <project> --json
Inspect comment / reply atoms basecamp comments show <url> --json → reply_target + mention in .data
List attachments basecamp attachments list <id\|url> --json
Download attachments basecamp attachments download <id> --out /tmp/
Show + download basecamp todos show <id> --download-attachments --json
Stream attachment to stdout basecamp attachments download <id> --file <name> --out -
Change history for an item basecamp events <id\|url> --json (when a card moved columns, when a todo was completed)
Account-wide activity feed (resumable) basecamp events poll --since now --json (then resume with --position)
Items that addressed me (agents only) basecamp inbox --since now --json
Search basecamp search "query" --json
Parse URL basecamp url parse "<url>" --json
Upload file basecamp files uploads create <file> [--vault <folder_id>] --in <project> --json
Download file basecamp files download <id> --in <project>
Stream file to stdout basecamp files download <id> --out - --in <project>
Download storage URL basecamp files download "https://storage.3.basecamp.com/.../download/report.pdf"
My assignments basecamp assignments --json (priorities + non-priorities)
Overdue assignments basecamp assignments due overdue --json
Completed assignments basecamp assignments completed --json
Notifications basecamp notifications --json
Mark notification read basecamp notifications read <id> --json
All bubble-ups (BC5) basecamp notifications bubbleups --json
Gauges (account-wide) basecamp gauges list --json
Gauge needles basecamp gauges needles --in <project> --json
Create needle basecamp gauges create --position 75 --color green --in <project> --json
Account details basecamp accounts show --json

URL Parsing

Parse URLs before acting on them — unless you're handing the URL to a command that accepts a URL directly (show, comments show, comments thread, attachments list/attachments download), which extract the IDs for you. Only comments show and comments thread verify the URL's host and account before any fetch. For other URL-accepting commands, only pass URLs from a trusted Basecamp host: basecamp url parse extracts IDs but does not validate the URL's origin, so parsing an attacker-controlled path yields trusted-looking IDs.

basecamp url parse "https://3.basecamp.com/2914079/buckets/41746046/messages/9478142982#__recording_9488783598" --json

Returns: account_id, project_id, type, recording_id, comment_id (from fragment).

URL patterns:

  • /buckets/27/messages/123 - Message 123 in project 27
  • /buckets/27/messages/123#__recording_456 - Comment 456 on message 123
  • /buckets/27/card_tables/cards/789 - Card 789
  • /buckets/27/card_tables/columns/456 - Column 456 (for creating cards)
  • /buckets/27/todos/101 - Todo 101
  • /buckets/27/uploads/202 - Upload/file 202
  • /buckets/27/documents/303 - Document 303
  • /buckets/27/schedule_entries/404 - Schedule entry 404

Replying to comments:

# Comments are flat - reply to the parent recording_id, not the comment_id
basecamp url parse "https://...messages/123#__recording_456" --json
# Returns recording_id: 123 (parent), comment_id: 456 (fragment) - comment on 123, not 456
basecamp comments create 123 "Reply" --in <project>

# Or get the whole reply-ready context deterministically in one call:
basecamp comments thread "https://...messages/123#__recording_456" --json
# .data.reply_target.recording_id  → where to post the reply
# .data.reply_target.account_id    → the account that reply belongs to (build a fully-qualified command)
# .data.focus.author.mention.syntax → paste-ready [@Name](mention:SGID)
# .data.comments                   → surrounding discussion (default window of 41)
# --all returns every fetched comment; --window N sets the window size
# When the account came from the URL (none configured), the reply breadcrumb carries --account

Decision Trees

Finding Content

Need to find something?
├── Know the type + project? → basecamp <type> list --in <project> --json
│   (some groups have default list behavior; use --agent --help if unsure)
├── My assigned work? → basecamp assignments --json (priorities + non-priorities)
│   Or: basecamp reports assigned --json (traditional view, defaults to "me")
├── My overdue assignments? → basecamp assignments due overdue --json
├── My notifications? → basecamp notifications --json
├── Upcoming schedule? → basecamp reports schedule --json (cross-project)
├── Overdue across projects? → basecamp reports overdue --json
├── Browse by type cross-project? → basecamp recordings <type> --json
│   (types: todos, messages, documents, comments, cards, uploads)
│   Note: Defaults to active status; use --status archived for archived items
│   ⚠ No assignee data — cannot filter by person; use reports assigned instead
├── Full-text search? → basecamp search "query" --json
├── Have a comment URL, or a notification link targeting a comment? → basecamp comments thread <url> --json
└── Have a URL? → basecamp url parse "<url>" --json

Modifying Content

Want to change something?
├── Have URL? → basecamp url parse "<url>" → use extracted IDs
├── Have ID? → basecamp <resource> update <id> --field value
├── Change status? → basecamp recordings trash|archive|restore <id>
├── Complete todo? → basecamp todos complete <id>
├── Complete card? → basecamp cards done <id|url> --in <project>
└── Reply to a comment? → basecamp comments show <url> --jq '.data | {reply_target, mention}'
    (one call, cheap atoms — the mention is machine-only, so use --jq/--json, not plain show)
    or basecamp comments thread <url> when you need the surrounding discussion;
    then basecamp comments create <reply_target.recording_id> <text>

Common Workflows

Link Code to Basecamp Todo

# Get commit info and comment on todo (use printf %q for safe quoting)
COMMIT=$(git rev-parse --short HEAD)
MSG=$(git log -1 --format=%s)
basecamp comments create <todo_id> "Commit $COMMIT: $(printf '%s' "$MSG")" --in <project>

# Complete when done
basecamp todos complete <todo_id>

Track PR in Basecamp

# Create todo for PR work
basecamp todos create "Review PR #42" --in <project> --assignee me --due tomorrow

# When merged
basecamp todos complete <todo_id>
basecamp chat post "Merged PR #42" --in <project>

Bulk Process Overdue Todos

# Preview overdue todos
basecamp todos sweep --overdue --dry-run --in <project>

# Complete all with comment
basecamp todos sweep --overdue --complete --comment "Cleaning up" --in <project>

Mentioning people (preferred — deterministic)

# 1. Look up the person
basecamp people pingable --jq '.data[] | select(.name == "Jane Smith")'
# => {"id": 42000, "attachable_sgid": "BAh7CEkiCG...", "name": "Jane Smith"}

# 2. Use SGID in Markdown mention syntax (zero API calls during post)
basecamp comments create 123 "Hey [@Jane Smith](mention:BAh7CEkiCG...), check this" --in <project>

# Or use person ID (one lookup during post)
basecamp comments create 123 "Hey [@Jane Smith](person:42000), check this" --in <project>

Mentioning people (interactive — may be ambiguous)

# Fuzzy matching: use @First.Last to reduce ambiguity
basecamp comments create <id> "@Jane.Smith, please review this" --in <project>
basecamp messages create "Update" "cc @Jane, @Alex" --in <project>
basecamp chat post "@Jane, done!" --in <project>

# Ambiguous names return an error with suggestions
# Use @First.Last for disambiguation

Move Card Through Workflow

# List columns to get IDs
basecamp cards columns --in <project> --json

# Complete a card (moves it to the Done column automatically)
basecamp cards done <card_id> --in <project>

# Move card to column
basecamp cards move <card_id> --to <column_id> --in <project>

# Move card to specific position in column (1-indexed)
basecamp cards move <card_id> --to <column_id> --position 1 --in <project>

# Move card to on-hold section of its current column
basecamp cards move <card_id> --on-hold --in <project>

# Move card to on-hold section of a specific column (numeric ID)
basecamp cards move <card_id> --to <column_id> --on-hold --in <project>

# Move card to on-hold section of a named column (requires --card-table)
basecamp cards move <card_id> --to "Column Name" --on-hold --card-table <table_id> --in <project>

Download File from Basecamp

basecamp files download <upload_id> --in <project> --out ./downloads

# Download attachment from a storage URL (no --in needed)
basecamp files download "https://storage.3.basecamp.com/123/blobs/abc/download/report.pdf"

# Stream to stdout (for piping)
basecamp files download <upload_id> --out - --in <project>

Working with Attachments (Multimodal Agent Workflow)

Messages, todos, cards, and documents may contain images and file attachments (mockups, screenshots, annotated designs). Show commands surface these as field-scoped collections — content_attachments and/or description_attachments — keyed by which rich-text attribute contained them. The notice field hints at the download command.

Step 1: Fetch the recording and check for attachments

basecamp todos show <id> --json
# Response includes description_attachments when attachments are present
# Messages/documents use content_attachments; cards may have both
# The notice field hints: "3 attachment(s) — download: basecamp attachments download <id>"

Step 2 (one-shot): Download attachments with the show command

# --download-attachments fetches + downloads in one shot
basecamp todos show <id> --download-attachments --json
# content_attachments/description_attachments entries now include "path" pointing to local files
# Downloads to OS temp dir by default, or specify: --download-attachments /tmp/att

Step 2 (two-step alternative): Download separately

# Download all at once (shows progress on stderr)
basecamp attachments download <id> --out /tmp/attachments

Step 3: View images with your native file-read tool For multimodal LLMs (Claude, Gemini), use your file-read tool on the path from the response to view downloaded images directly — no browser needed. This surfaces visual context (mockups, screenshots, annotated designs) that is often the most important part of a Basecamp todo or message.

# Stream a single image to stdout for piping
basecamp attachments download <id> --file mockup.png --out -

# Select by index when names collide
basecamp attachments download <id> --index 2 --out -

Key pattern: When a show command response contains content_attachments or description_attachments, always download and view them — visual context is often more important than the text content. Use --download-attachments for one-shot fetch+download, or follow the breadcrumb hint for two-step control.

Resource Reference

Projects

basecamp projects list --json               # List all
basecamp projects show <id> --json          # Show details
basecamp projects create "Name" --json      # Create
basecamp projects update <id> --name "New"  # Update
basecamp projects trash <id>                # Move to trash (recoverable)

Archiving a project: the CLI does not have a dedicated archive command, but the underlying status endpoint can be hit via raw API. Same path works for restoring to active or moving to trashed.

basecamp api put "projects/<id>/status/archived" -d '{}' --json   # Archive
basecamp api put "projects/<id>/status/active" -d '{}' --json     # Unarchive
basecamp api put "projects/<id>/status/trashed" -d '{}' --json    # Trash (same as `projects trash`)

Verify with basecamp projects show <id> --jq '.data.status'.

Todos

basecamp todos list --in <project> --json               # List in project
basecamp todos list --assignee me --in <project>        # My todos
basecamp todos list --overdue --in <project>            # Overdue only
basecamp todos list --status completed --in <project>   # Completed
basecamp todos list --list <todolist_id> --in <project> # In specific list
basecamp todos create "Task" --in <project> --list <list> --assignee me --due tomorrow
basecamp todos complete <id> [id...]                    # Complete (multiple OK)
basecamp todos uncomplete <id>                          # Reopen
basecamp assign <id> [id...] --to <person> --in <project>       # Assign to-do (multiple OK)
basecamp unassign <id> [id...] --from <person> --in <project>   # Remove to-do assignee (multiple OK)
basecamp assign <id> [id...] --card --to <person> --in <project>   # Assign card
basecamp unassign <id> [id...] --card --from <person> --in <project> # Remove card assignee
basecamp assign <id> [id...] --step --to <person> --in <project>   # Assign card step
basecamp unassign <id> [id...] --step --from <person> --in <project> # Remove step assignee
basecamp todos position <id> --to 1                     # Move to top
basecamp todos position <id> --to 1 --list <id|name|url> # Move to different list
basecamp todos sweep --overdue --complete --comment "Done" --in <project>
basecamp todos create "Task" --in <project> --list <list> --notify-on-completion "Jane,Bob"  # Notify when done
basecamp todos update <id> --notify-on-completion "Jane"  # Set who's notified on completion
basecamp todos update <id> --no-notify-on-completion      # Clear completion notifications

Flags: --assignee (repeatable; server-side account-wide, client-side within a project; also on cards list account-wide, but not on messages), --status (completed/incomplete/archived/trashed), --overdue, --list, --due (listing filter: with/without/overdue only, account-wide only — not a date; see Smart Defaults), --limit, --all

Completion subscribers ("When done, notify…"): set with --notify-on-completion <names or IDs, comma-separated> on todos create and todos update; clear with --no-notify-on-completion on todos update. Plain updates (title, due date, etc.) preserve existing completion subscribers.

Todo Subtasks (checklist steps): Basecamp to-do subtasks are stored as Kanban::Step records, even when their parent is a normal Todo. The regular basecamp todos show response may not include them; use basecamp recordings list --in <project> --type Kanban::Step and filter by parent.id to list/check subtasks for a todo.

# Create a subtask under a todo.
# Use the numeric project ID and todo ID in this card-style path.
basecamp api post /buckets/<project_id>/card_tables/cards/<parent_todo_id>/steps.json \
  --data '{"title":"Subtask title"}' \
  --json

# Read or edit a subtask
basecamp api get /buckets/<project_id>/card_tables/steps/<step_id>.json --json
basecamp api put /buckets/<project_id>/card_tables/steps/<step_id>.json \
  --data '{"title":"Updated subtask title"}' \
  --json

# List subtasks for a todo
PARENT_TODO_ID=<parent_todo_id> \
basecamp recordings list --in <project> --type Kanban::Step --all \
  --jq '.data[] | select(.parent.id==(env.PARENT_TODO_ID | tonumber)) | {id,title,status,parent:.parent.id,url}'

# Assign or set a due date. Send only what you're changing — omitted fields are
# left alone. `assignee_ids` replaces the whole list, so name everyone who stays.
basecamp api put /buckets/<project_id>/card_tables/steps/<step_id>.json \
  --data '{"assignee_ids":[<person_id>,<existing_person_id>],"due_on":"<YYYY-MM-DD>"}' \
  --json

# Complete or reopen a subtask
basecamp api put /buckets/<project_id>/card_tables/steps/<step_id>/completions.json \
  --data '{"completion":"on"}' \
  --json
basecamp api put /buckets/<project_id>/card_tables/steps/<step_id>/completions.json \
  --data '{"completion":"off"}' \
  --json

# Trash a subtask from the todo UI by trashing the step record (Kanban::Step)
basecamp recordings trash <step_id> --in <project> --json

Key points: replace numeric placeholders such as <project_id>, <parent_todo_id>, and <person_id> before running the examples. Bucket-scoped API paths require a numeric project/bucket ID; --in <project> can still accept a project name where CLI commands support name resolution. For creating todo subtasks, Basecamp accepts the parent todo ID in the /buckets/<project_id>/card_tables/cards/<parent_todo_id>/steps.json path. To list subtasks under a todo, use basecamp recordings list --in <project> --type Kanban::Step with the parent.id filter shown above.

Completed subtasks have completed: true and a completion object with created_at and creator. Open subtasks have completed: false and no completion object. Trashed subtasks may still be readable directly with status: "trashed" and inherits_status: false, but they no longer appear in the todo UI.

In testing with todo-backed steps, these bucket-scoped direct GET requests returned not_found: /buckets/<project_id>/card_tables/cards/<parent_todo_id>/steps.json, /buckets/<project_id>/card_tables/cards/<parent_todo_id>.json, and /buckets/<project_id>/todos/<parent_todo_id>/steps.json. To inspect trashed subtasks, add --status trashed; archived parents may require --status archived.

Raw step updates are partial. PUT .../card_tables/steps/<id>.json leaves every parameter you omit unchanged, so send only the fields you are changing. Echoing back a title you did not mean to change is not merely redundant — it reverts anyone who edited the title between your read and your write. To clear a value, say so explicitly: "due_on": null clears the due date, "assignee_ids": [] removes everyone. assignee_ids always replaces the whole list rather than adding to it, so name every person who should remain assigned.

(This is bc3#12521. Before it, an omitted field was cleared and a title-less update was rejected, which is why older guidance said to resend the title. Todo subtasks and card steps share one endpoint and one contract — PUT card_tables/steps/:id routes to the same controller for both.)

The generic basecamp assign <step_id> --step ... command is intended for card steps and may fail with Bad Request for todo-backed steps, so prefer assignee_ids on the raw step update endpoint for todo subtasks.

Todolists

Todolists are containers for todos. Create a todolist before adding todos.

basecamp todolists list --in <project> --json              # List todolists
basecamp todolists show <id> --in <project>                # Show details
basecamp todolists create "Name" --in <project> --json     # Create
basecamp todolists create "Name" --description "Desc" --in <project>
basecamp todolists create "Name" --visible-to-clients --in <project>  # Visible to clients
basecamp todolists update <id> --name "New" --in <project> # Update
basecamp todolists position <id> --to 1                     # Reorder one list (1 = top)
basecamp todolists position <id> <id> <id>                  # Order incomplete lists, top→bottom

Bulk position sets the visible order in one command: pass incomplete lists from the same todoset, top to bottom. It always places them at the top.

Cards (Kanban)

Note: --assignee on cards list is account-wide only — pass --all-projects (or have no project in scope) and it becomes a real server-side filter. Within a single project cards have no assignee filter: fetch all and filter client-side. --due with|without|overdue is account-wide only on cards too. If a project has multiple card tables, you must specify --card-table <id>. When you get an "Ambiguous card table" error, the hint shows available table IDs and names.

basecamp cards list --in <project> --json             # All cards
basecamp cards list --card-table <id> --in <project>  # Specific table (required if multiple)
basecamp cards list --column <id> --in <project>      # Cards in column
basecamp cards columns --in <project> --json          # List columns (needs --card-table if multiple)
basecamp cards show <id> --in <project>               # Card details
basecamp cards create "Title" "<p>Body</p>" --in <project> --column <id>
basecamp cards update <id> --title "New" --due tomorrow --assignee me
basecamp cards done <id|url> --in <project>           # Move to the Done column automatically
basecamp cards move <id> --to <column_id>             # Move to column (numeric ID)
basecamp cards move <id> --to "Done" --card-table <table_id>  # Move by name (needs table)
basecamp cards move <id> --to "Done" --position 1 --card-table <table_id>  # Move to position
basecamp cards move <id> --on-hold                    # Move to on-hold of current column
basecamp cards move <id> --to <column_id> --on-hold   # Move to on-hold of target column

Cross-project card move (wormholes): the only way to move a card to another project is to teleport it through a wormhole — a portal on the card table that sends cards to a preconfigured column on another project's card table (max 4 per table). The teleport is asynchronous and mints a new card id: after the move is accepted, the server copies the card into the destination and deletes the original, so the original id 404s — do not reuse it.

basecamp cards wormholes list --in <project>          # Discover wormholes (id, destination, linked)
basecamp cards wormholes create --to-column <id|url> --in <project>   # Link to a column on another table (≤4)
basecamp cards wormholes update <id> --to-column <id|url> --in <project>
basecamp cards wormholes delete <id> --in <project>
basecamp cards move <card_id> --to-wormhole <wormhole_id> --in <project>          # Teleport (async)
basecamp cards move <card_id> --to-wormhole <destination_column_url> --in <project>  # Match by destination column

--to-wormhole is mutually exclusive with --to/--on-hold/--position. Pass a numeric wormhole id to route directly, or a destination-column URL to match it against the source table's wormholes.

Archived/trashed cards: cards list only returns active cards. For archived or trashed cards, use basecamp recordings cards --status archived --in <project> or --status trashed.

Identifying completed cards: Cards in Done columns have parent.type: "Kanban::DoneColumn" and completed: true. Use this to identify completed cards that haven't been archived.

When a card moved columns: don't read updated_at — it changes on any modification. Use the event history instead: basecamp events <card_id> --json records an adopted event for every column move, and a card crossing into or out of a Done column pairs that with completed/uncompleted. See Events.

Card Steps (checklists):

basecamp cards steps <card_id> --in <project>     # List steps
basecamp cards step create "Step" --card <id> --in <project>
basecamp cards step complete <step_id> --in <project>
basecamp cards step uncomplete <step_id>

Column management:

basecamp cards column show <id> --in <project>
basecamp cards column create "Name" --in <project>
basecamp cards column update <id> --title "New"
basecamp cards column move <id> --position 2
basecamp cards column color <id> --color blue
basecamp cards column on-hold <id>                # Enable on-hold section
basecamp cards column watch <id>                  # Subscribe to column

Messages

basecamp messages list --in <project> --json  # List messages
basecamp messages show <id> --in <project>    # Show message
basecamp messages create "Title" "Body" --in <project>
basecamp messages create "Draft" "WIP" --draft --in <project>  # Create draft
basecamp messages publish <id>               # Publish a draft
basecamp messages update <id> --title "New" --body "Updated"
basecamp messages pin <id> --in <project>     # Pin to top
basecamp messages unpin <id>                  # Unpin

Archived/trashed messages: messages list only returns active messages. For archived or trashed messages, use basecamp recordings messages --status archived --in <project> or --status trashed.

Flags: --draft (create as draft), --no-subscribe (silent, no notifications), --subscribe "people" (comma-separated names, emails, IDs, or "me"; mutually exclusive with --no-subscribe), --message-board <id> (if multiple boards), --visible-to-clients (make visible to clients on the project; omit for the server default)

basecamp messages create "Bot update" "Done" --no-subscribe --in <project>
basecamp messages create "FYI" "Note" --subscribe "Alice,bob@x.com" --in <project>
basecamp messages create "For the client" "..." --visible-to-clients --in <project>

Client visibility at create time: messages create, todolists create, schedule create, checkins question create, and tools create accept --visible-to-clients to post a client-visible recording in one call (for tools create, only chat and kanban_board tool types honor it — other types inherit the project default). Omitting the flag uses the server default, which is context-dependent: team-only when you post as a team member, but a client-authenticated caller always creates client-visible records (an explicit --visible-to-clients=false is overridden server-side for client callers). Passing --visible-to-clients posts client-visible in every case. To change visibility on an already-created recording, use recordings visibility <id> --visible.

Comments

basecamp comments list <recording_id> --in <project> --json
basecamp comments show <comment-id|comment-url> --json            # Now returns reply_target + paste-ready mention (JSON)
basecamp comments thread <comment-id|comment-url> --json          # Reply-ready: parent + focus + discussion + @mention
basecamp comments thread <comment-id> --all --json                # Every fetched comment instead of a window
basecamp comments thread <comment-id> --window 11 --json          # Focus-centered window of 11
basecamp comments create <recording_id> "Text" --in <project>
basecamp comments create <recording_id> "@Jane.Smith, looks good!" --in <project>  # With @mention
basecamp comments update <id> "Updated" --in <project>

Cheap atoms vs. deep context (choose by need):

  • comments show <url> --jq '.data | {reply_target, mention}' — one API call. Returns reply_target (recording_id — where a reply is posted, comments are flat — plus account_id) and a paste-ready author mention (JSON only; human output shows a reply breadcrumb). Use this for the exact-comment reply atoms.
  • comments thread <url> — two extra calls. Adds the full parent recording, the surrounding discussion (windowed, truncation-honest), and focus attachments. Use this when the surrounding discussion matters.

Files & Documents

basecamp files list --in <project> --json               # List all (folders, files, docs)
basecamp files list --vault <folder_id> --in <project>  # List folder contents
basecamp files list --all-projects --json               # Across every project (first 100)
basecamp files list --all-projects --limit 500          # Walk pages until 500 collected
basecamp files list --all-projects --page 2             # Exactly page 2
basecamp files list --all-projects --all                # Every page (slow on big accounts)
basecamp files show <id> --in <project>                 # Show item (auto-detects type)
basecamp files versions <upload_id> --json              # Every version of an uploaded file
basecamp files versions <upload_id> --limit 5 --json    # Cap results (default: all)
basecamp files replace <upload_id> <file>               # Replace the file, keep the ID/URL/comments
basecamp files replace <upload_id> <file> --description "v2 notes"  # Also set a new description
basecamp files download <id> --in <project>             # Download file
basecamp files download <id> --out ./dir                # Download to specific dir
basecamp files download "https://storage.../download/f" # Download from storage URL
basecamp files uploads create <file> --in <project>      # Upload file to root
basecamp files uploads create <file> --vault <folder_id> --in <project>  # Upload to folder
basecamp files uploads create <file> --visible-to-clients --in <project>  # Client-visible (root folder only)
basecamp files folder create "Folder" --in <project>
basecamp files doc create "Doc" "Body" --in <project>
basecamp files doc create "Draft" --draft --in <project>
basecamp files doc create "Notes" "..." --no-subscribe --in <project>
basecamp files doc create "For client" "..." --visible-to-clients --in <project>  # Client-visible (root folder only)
basecamp files update <document_id> --title "New" --content "Updated"
basecamp files update <document_id> --title "New" --in <project>      # Preserves existing document content
basecamp files update <document_id> --content "Updated" --in <project> # Preserves existing document title

Document update semantics: basecamp files update <document_id> is safe for partial updates in the CLI: when you pass only --title or only --content, the CLI first fetches the current document and preserves the untouched field.

Client visibility at create time: doc create and uploads create accept --visible-to-clients, but the server only honors it in the project's root Docs & Files folder. Targeting a nested folder (--vault/--folder) with the flag is a hard error raised before anything is uploaded — a nested item inherits its folder's visibility, and that can't be changed per-item afterward (the visibility endpoint rejects nested docs/uploads). To make a nested item client-visible, create it in the root folder, or change the eligible top-level ancestor that controls the folder's visibility first. Omitting the flag uses the server default; as with Messages, a client-authenticated caller always creates client-visible records regardless. recordings visibility is not a remediation for nested docs/uploads.

Upload versions: replacing a file keeps the earlier copies under the same upload ID, so basecamp files versions <upload_id> is how you see the history of one file. A file that was never replaced returns its single current version, not an error. Only --page 1 is accepted; use --all to walk every page. basecamp files replace <upload_id> <file> publishes a new version in place — the upload keeps its ID, URL and comments, nobody is notified, and the description carries forward unless --description is given. Use it instead of uploads create when shipping a new build of the same file.

Subcommands: folders, uploads, documents (each with pagination flags)

Schedule

For upcoming events across all projects, use basecamp reports schedule --json.

basecamp schedule info --in <project> --json       # Schedule info
basecamp schedule entries --in <project> --json   # List entries
basecamp schedule show <id> --in <project>        # Entry details
basecamp schedule show <id> --date 20240315       # Specific occurrence (recurring)
basecamp schedule create "Event" --starts-at "2024-03-15T09:00:00Z" --ends-at "2024-03-15T10:00:00Z" --in <project>
basecamp schedule create "Meeting" --all-day --notify --participants 1,2,3 --in <project>
basecamp schedule create "Sync" --starts-at "..." --ends-at "..." --no-subscribe --in <project>
basecamp schedule update <id> --summary "New title" --starts-at "..."
basecamp schedule settings --include-due --in <project>  # Include todos/cards due dates

Flags: --all-day, --notify, --participants <ids>, --no-subscribe, --subscribe "people" (mutually exclusive), --status (active/archived/trashed), --visible-to-clients (make visible to clients; omit for the server default)

Check-ins

basecamp checkins --in <project> --json           # Questionnaire info
basecamp checkins questions --in <project>        # List questions
basecamp checkins question <id> --in <project>    # Question details
basecamp checkins answers <question_id> --in <project>  # List answers
basecamp checkins answers <question_id> --by me --in <project>  # My answers only
basecamp checkins answers <question_id> --by "Alice Smith" --in <project>  # Filter by person (name, email, or ID)
basecamp checkins answer <id> --in <project>      # Answer details
basecamp checkins question create "What did you work on?" --in <project>
basecamp checkins question update <id> "New question" --frequency every_week
basecamp checkins answer create <question-id> "My answer" --in <project>  # Defaults to today
basecamp checkins answer update <id> "Updated" --in <project>

Schedule options: --frequency (every_day, every_week, every_other_week, every_month, on_certain_days), --days 1,2,3,4,5 (0=Sun), --time "5:00pm"

Client visibility: checkins question create accepts --visible-to-clients to make the question visible to clients (omit for the server default; see the note under Messages for the context-dependent rule).

Managing a question:

basecamp checkins question pause <id> --json      # Stop asking it
basecamp checkins question resume <id> --json     # Start asking it again
basecamp checkins question answerers <id> --json  # Who answers it
basecamp checkins question notify <id> --on-answer --json
basecamp checkins question notify <id> --no-on-answer --json
basecamp checkins question notify <id> --digest-include-unanswered --json

notify changes your own settings, and each one is left alone unless you name it — so --on-answer does not silently reset the digest setting. The --no-... spellings send an explicit false; passing neither setting is refused rather than sent as an empty update.

Your pending reminders (account-wide, no --in):

basecamp checkins reminders --json
basecamp checkins reminders --limit 10 --json

reminders and answerers take --limit but deliberately no --page: the API does not honor a page number on these, so the flag would accept a value it could not act on.

Timeline

basecamp timeline --json                          # Account-wide activity
basecamp timeline --in <project> --json           # Project activity
basecamp timeline me --json                       # Your activity
basecamp timeline --person <id> --json            # Person's activity

Use --limit N to cap results or --all to fetch everything (default: 100 events).

Events (change history)

basecamp timeline reports activity across a project or account. For the audit trail of one specific item — todo, card, message, document — use basecamp events:

basecamp events <id|url> --json                   # Change history for one item
basecamp events <id> --limit 25 --json            # Cap results (default 100)
basecamp events <id> --all --json                 # Fetch everything

Common action values: created, completed/uncompleted, assignment_changed, content_changed, archived/unarchived, commented_on, and — for cards — adopted, which is recorded every time a card moves to another column. That makes events the way to answer "when did this card move?" or "when was this actually finished?", neither of which updated_at can tell you.

--page accepts only 1; use --all to walk every page.

Event feed (account-wide, resumable)

basecamp events <id> is one recording's history. The account-wide event feed is a different resource, and the way an agent hears about activity it did not cause:

basecamp events poll --since now --json            # Enter at the present
basecamp events poll --position "$POSITION" --json # Resume from a held position
basecamp events poll --since 0 --all --json        # Replay served history
basecamp inbox --since now --json                  # Addressed items (agents only)
basecamp events ticket --json                      # Mint a live-stream ticket

Each page is an envelope: events (or items), a durable position, and a next continuation URL while the current walk has more to serve. Persist position only after processing the page, then pass it back with --position. The response's notice spells the whole resume command out, filters included. --all walks next to the end of the current walk — it is not a live tail.

The feed is a notification lane, not an audit log:

  • Deduplicate by event id — polls repeat what the live stream delivered, and an event can appear up to ~30 seconds after it happens.
  • Deduplicate inbox items by addressing_id, never by event id: one event addresses you once per reason and each reason is its own item.
  • Events are thin pointers. Refetch the recording (basecamp show <recording_id>) before acting on it.
  • An agent that acts on what it hears should pass --exclude-performers self, which drops its own performances without hiding other agents' activity.

The two lanes have different filter sets, and they are not interchangeable:

  • events poll: --types, --buckets, --creators, --performers, --exclude-performers, --actor-types.
  • inbox: --reasons, --types, --buckets. The other four are not flags here.

Each takes a comma-separated list. These are the only narrowing available: both commands accept the global --in <project> flag but ignore it, because these are account endpoints. Narrow to a project with --buckets.

A position is bound to the filter set it was minted for, so a resume has to carry the same filters. Changing them exits 1 naming both filter digests, and the fix is to re-enter with --since. A position that is no longer servable exits 2, and the hint carries the whole re-entry command, filters included.

basecamp inbox is served to agent principals only for now; other principals get exit 4. basecamp events ticket redacts the ticket and its URL unless --show-secret is passed — both are bearers, so never log either, and mint a fresh one per connection attempt.

Recordings (Cross-project)

Use basecamp recordings <type> for cross-project type browsing. For assigned todos, prefer basecamp reports assigned — recordings do not include assignee data and cannot be filtered by person.

basecamp recordings todos --json                  # All todos across projects
basecamp recordings todos --all --json            # All todos (paginate through all)
basecamp recordings messages --in <project>       # Messages in project
basecamp recordings documents --status archived   # Archived docs
basecamp recordings cards --sort created_at --direction asc
basecamp recordings cards --status archived --all --json  # Include archived cards

Types: todos, messages, documents, comments, cards, uploads

Status filtering: By default, only active recordings are returned. Use --status archived or --status trashed to query other statuses. You may need separate queries to get complete data (e.g., active + archived).

Status management:

basecamp recordings trash <id> --in <project>     # Move to trash
basecamp recordings archive <id> --in <project>   # Archive
basecamp recordings restore <id> --in <project>   # Restore to active
basecamp recordings visibility <id> --visible --in <project>  # Show to clients
basecamp recordings visibility <id> --hidden      # Hide from clients

Templates

basecamp templates list --json                    # List project templates
basecamp templates show <id> --json               # Project template details
basecamp templates create "Template Name"         # Create empty project template
basecamp templates update <id> --name "New Name"
basecamp templates delete <id>                    # Trash project template
basecamp templates construct <id> --name "New Project"  # Create project (async)
basecamp templates construct <id> --name "New Project" --start-date 2026-09-01  # Anchor template dates to that week
basecamp templates construction <template_id> <construction_id>  # Check project status

basecamp templates library --json                 # List active to-do list templates
basecamp templates copy <template_id> --in <project>  # Start copying into To-dos
basecamp templates copy-status <copy_id>          # Check copy status

Asynchronous results: construct returns a construction ID; poll construction until status="completed" to get the project. A template's dates are relative to its first week, and template weeks start on Sunday: --start-date (YYYY-MM-DD or natural, e.g. next monday) anchors them to the Sunday on or before that date; without it they anchor to the week of construction. copy returns a copy ID; poll copy-status through pending and processing until it is completed or failed.

A copy can report the people who need access to the destination project. Show those people to the user and rerun with `--confirm-adding-p

Files (basecamp-cli)
  • SKILL.md 85.7 KB
    ---
    name: basecamp
    description: |
      Interact with Basecamp via the Basecamp CLI. Full API coverage: projects, todos, cards,
      messages, files, schedule, check-ins, timeline, recordings, templates, webhooks,
      subscriptions, lineup, chat, pings, gauges, assignments, notifications, bookmarks,
      bubble-up, drafts, notes, calendars, and accounts.
      Use for ANY Basecamp question or action.
    triggers:
      # Direct invocations
      - basecamp
      - /basecamp
      # Resource actions
      - basecamp todos
      - basecamp project
      - basecamp cards
      - basecamp chat
      - basecamp campfire
      - basecamp messages
      - basecamp file
      - basecamp document
      - basecamp bookmarks
      - basecamp bubble-up
      - basecamp drafts
      - basecamp notes
      - basecamp calendars
      - basecamp schedule
      - basecamp checkin
      - basecamp check-in
      - basecamp timeline
      - basecamp template
      - basecamp webhook
      - basecamp gauge
      - basecamp assignment
      - basecamp notification
      - basecamp account
      # Common actions
      - link to basecamp
      - track in basecamp
      - post to basecamp
      - comment on basecamp
      - complete todo
      - mark done
      - create todo
      - move card
      - download file
      # Search and discovery
      - search basecamp
      - find in basecamp
      - look up basecamp
      - check basecamp
      - list basecamp
      - show basecamp
      - get from basecamp
      - fetch from basecamp
      # Questions
      - can I basecamp
      - how do I basecamp
      - what's in basecamp
      - what basecamp
      - does basecamp
      # My work
      - my todos
      - my tasks
      - my schedule
      - my basecamp
      - assigned to me
      - my assignments
      - my notifications
      - overdue todos
      - upcoming events
      - project gauge
      - project progress
      # URLs
      - 3.basecamp.com
      - basecampapi.com
      - https://3.basecamp.com/
    invocable: true
    argument-hint: "[action] [args...]"
    ---
    
    # /basecamp - Basecamp Workflow Command
    
    Full CLI coverage: 195 tracked in-scope endpoints across todos, cards, messages, files, schedule, check-ins, timeline, recordings, templates, webhooks, subscriptions, lineup, chat, pings, gauges, assignments, notifications, the account event feed, and accounts.
    
    ## Agent Invariants
    
    **MUST follow these rules:**
    
    1. **Choose the right output mode** — `--jq` when you need to filter/extract data; `--json` for full JSON; `--md` when presenting results to a human (see Output Modes below). **Never pipe to external `jq` — use `--jq` instead.**
    2. **Parse URLs first** with `basecamp url parse "<url>"` to extract IDs
    3. **Comments are flat** - reply to parent recording, not to comments
    4. **Check context** via `.basecamp/config.json` before assuming project
    5. **Content fields accept Markdown, and most accept @mentions** — the CLI converts these rich-text fields from Markdown to HTML: message bodies, document bodies, comment content, todo descriptions, card bodies, schedule entry descriptions, upload descriptions, check-in answers and notes. Two rich-text fields are sent as written, so give them HTML: todolist descriptions and gauge needle descriptions. Chat is different again: `chat post` sends plain text unless you pass `--content-type text/html` or the line carries a mention. Use Markdown formatting (lists, bold, links, code blocks, tables) for rich content. @mentions resolve in message bodies, comment content, card bodies, schedule descriptions and chat lines — not in todo descriptions, documents, uploads, check-ins or notes. Four mention syntaxes are available (prefer deterministic for agents):
       - **`[@Name](mention:SGID)`** — zero API calls, embeds SGID directly (preferred for agents)
       - **`[@Name](person:ID)`** — one API call, resolves person ID to SGID via pingable set
       - **`@sgid:VALUE`** — inline SGID embed for pipeline composability
       - **`@Name` / `@First.Last`** — fuzzy name resolution (may be ambiguous)
    
       Raw HTML is also accepted, but it is all-or-nothing per field: a tag the CLI detects as HTML (`<p>`, `<ul>`, `<strong>`, `<a>`, `<img>`, `<table>` and the other common formatting tags) outside a backtick code span or backtick fence (a `~~~` fence does not hide it) skips Markdown conversion for the whole field, so any Markdown alongside it — `![alt](/local/path)` included — is sent literally. The HTML itself goes through as written, except that an empty separator paragraph is inserted between directly adjacent `<p>` blocks so they render with spacing; a local path in a raw `<img src>` is still uploaded and replaced with an attachment in every converted field except notes, which take no attachments. Titles (a todo's content argument, card and message titles) are plain text and never converted.
    
       **Table boundary:** GFM tables round-trip: they render in message/comment
       bodies, display converts them back to pipe tables, and the TUI in-place
       editors open simple grids for editing. Only **complex** tables — merged
       cells (colspan/rowspan), captions, extra header rows, nested tables,
       attachments/images or block content inside cells, multi-paragraph or
       multi-line cells, or a table inside a blockquote or list — refuse to open,
       since a GFM pipe table can't represent those shapes (edit them on Basecamp
       web, or replace the
       whole field via `messages update` / `comments update` / `todos update
       --description`, which take fresh content and are unaffected). Complex
       tables still **display** best-effort, flattened to a plain grid.
    
       **Multiline / non-ASCII content:** do not rely on bash ANSI-C quoting (`$'...\n...'`) — it is a bash/zsh extension. Under a POSIX `/bin/sh` (dash, busybox-ash, common in sandboxes) the `$` is passed through literally and posts a stray leading `$`, and `\n` stays a literal backslash-n. Pipe the content via stdin instead, using `-` as the content argument:
       ```bash
       printf '%s\n' '海报 mockup 方向稿:' '' '<bc-attachment ...>' | basecamp comments create <recording_id> - --in <project> --json
       ```
       `-` means "read from stdin" on every content input: content-kind positionals
       (`comments create/update`, `messages create [body]`, `cards create [body]`,
       `todos create`, `docs documents create [content]`, `chat post/update`, `boost create`,
       `checkins answer create/update`, `notes set`) and content flags (`--data` on
       `api post/put`, `--body`, `--content`, `--description`, `--comment` on
       `todos sweep`, `--file` on `notes set`). Each command's `--agent` help lists
       its stdin inputs. Rules:
       - A pipe is **never consumed implicitly** — without `-` it is ignored (or, where
         content is required and missing, the error teaches `-`).
       - Only one input can read stdin per invocation.
       - A literal `-` anywhere else (a title, a name, a path) **errors when stdin is
         piped**. Escape a positional after the `--` separator
         (`basecamp projects create -- -`); a flag value has no in-line escape — run
         the command without piped stdin. `basecamp help` and shell completion are
         exempt: they write nothing to Basecamp, and completion legitimately
         receives `-` as the word being completed.
       - `-` with nothing piped (interactive TTY) errors immediately instead of
         hanging; use a pipe, a heredoc (`basecamp comments create <id> - <<'EOF'`),
         or `--edit` where offered.
       - Trailing newlines are trimmed from stdin content, so `printf 'x\n' | ... -`
         posts `x` (this keeps `boost create -` inside its 16-rune limit).
      - Universal `-` support (and the stray-`-` guard) shipped in **v0.10.0**. Older
        CLIs do not support it consistently: `comments create/update` read stdin,
        while unsupported inputs may treat `-` as literal content or fail. For
        example, `messages create "Title" -` posts a body of `-`, which Markdown
        renders as an empty bullet list. When the CLI version is unknown, check
        `basecamp --version` first, or pass the content portably as
        `"$(cat file.md)"` and verify the posted `content` when it matters.
    6. **Project scope is mandatory for most commands** — via `--in <project>` or `.basecamp/config.json`. Cross-project exceptions: `basecamp reports assigned` for assigned work, `basecamp assignments` for structured assignment views, `basecamp reports overdue` for overdue todos, `basecamp reports schedule` for upcoming schedule across all projects, `basecamp recordings <type>` for browsing by type, `basecamp notifications` for notifications, `basecamp gauges list` for account-wide gauges, `basecamp events poll` and `basecamp inbox` for the account event feed, and the seven list commands covered in item 7.
    7. **Account-wide listing.** `basecamp todos list --all-projects --json` lists across every project; the same flag does the same on `cards list`, `messages list`, `comments list`, `files list`, `forwards list`, and `checkins answers`. It overrides a configured project, and with no project in scope those commands already list account-wide rather than prompting. Flags that name something inside a single project are rejected there rather than silently ignored.
       Account-wide listings return **the first 100 items by default** — account-wide "all" is the whole account, not one project's worth. Use `--limit N` to raise the cap (it walks pages until N are collected) or `--all` for everything. `--page N` fetches exactly one page, but only on the paginated listings.
       The two overdue variants — `basecamp todos list --all-projects --overdue` and `basecamp cards list --all-projects --overdue` — come from unpaginated endpoints. They accept `--limit` and `--all` but **reject `--page`**, so do not generate `--page` against them.
    
    ### Output Modes
    
    **Choosing a mode:**
    
    | Goal | Flag | Format |
    |------|------|--------|
    | Filter/extract JSON data | `--jq '<expr>'` | Built-in jq filter (no external jq needed). Implies `--json`; filter runs on the envelope. |
    | Filter in agent mode | `--agent --jq '<expr>'` | Filter runs on data-only payload (no envelope), matching `--agent` contract. |
    | Full JSON output | `--json` | JSON envelope: `{ok, data, summary, breadcrumbs, meta}`; errors: `{ok:false, error, code, retryable, hint, meta}` |
    | Show results to a user | `--md` / `-m` | GFM tables, task lists, structured Markdown |
    | Automation / scripting | `--agent` | Success: raw JSON data (no envelope); errors: `{ok:false,...}` object; no interactive prompts |
    
    Always pass `--json` or `--md` explicitly — auto-detection depends on config and may not produce the format you expect. Use `--md` when composing reports, summarizing data, or displaying results inline. `--agent` is for headless integration scripts.
    
    **Avoiding interactive prompts.** The flags `--agent`/`--json`/`--quiet`/`--ids-only`/`--count` and the environment variable `BASECAMP_NONINTERACTIVE=1` suppress interactive selection prompts. `--md` does **not** — if a required target is ambiguous (e.g. a project with multiple todosets and no `--todoset`), and the CLI is attached to a terminal, it will show a blocking picker. When you need Markdown output *and* no prompts, either pass the flag that names whatever is ambiguous (`--todoset <id>` for the todoset case above, or `--in <project>` / `--list <id>` when the project or list is ambiguous) or set `BASECAMP_NONINTERACTIVE=1` in the environment. `BASECAMP_NONINTERACTIVE` disables all prompts (they become actionable errors instead) without changing the output format — an escape hatch for agents running under a PTY.
    
    **Other modes:** `--quiet` (success: raw JSON, no envelope; errors: `{ok:false,...}`), `--ids-only`, `--count`, `--stats` (session statistics), `--styled` (force ANSI), `-v` / `-vv` (verbose/trace), `--jq '<expr>'` (built-in jq filter — see below).
    
    ### CLI Introspection
    
    Navigate unfamiliar commands with `--agent --help` — returns structured JSON describing any command:
    
    ```bash
    basecamp todos --agent --help
    ```
    
    ```json
    {"command":"todos","path":"basecamp todos","short":"...","long":"...","usage":"...","notes":["..."],
     "subcommands":[{"name":"sweep","short":"...","path":"basecamp todos sweep"}],
     "flags":[{"name":"assignee","type":"string","default":"","usage":"..."}],
     "inherited_flags":[{"name":"json","shorthand":"j","type":"bool","default":"false","usage":"..."}]}
    ```
    
    Walk the tree: start at `basecamp --agent --help` for top-level commands, then drill into any subcommand. Commands carry domain-specific agent hints (e.g., "`--assignee` filters the account-wide listing only; within a project, fetch all and filter client-side").
    
    **Note:** a subcommand's `inherited_flags` is deliberately short — the CLI curates it down to `--account`, `--json`, `--md`, `--project`, `--quiet` (and drops `--project` where the command takes `<id|url>`). Every other global flag (`--jq`, `--agent`, `--styled`, `--verbose`, `--profile`, ...) is listed only at the root (`basecamp --agent --help`) but still applies on every subcommand (a command that cannot honor one refuses it with an explicit error — `version` rejects `--jq`); its absence from a subcommand's help does not mean it is unsupported.
    
    ### Pagination
    
    ```bash
    basecamp <cmd> --limit 50   # Cap results (default varies by resource)
    basecamp <cmd> --all        # Fetch all (may be slow for large datasets)
    basecamp <cmd> --page 1     # First page only, no auto-pagination
    ```
    
    `--all` and `--limit` are mutually exclusive. `--page` cannot combine with either.
    
    ### Smart Defaults
    
    - `--assignee me` resolves to current user
    - `--due tomorrow` / `--due +3` / `--due "next week"` — natural date parsing, **when setting a due date** (`todos create`, `todos update`, `cards create`, and so on)
    - `--due` on a **listing** is a different flag and does not take dates: it accepts only `with`, `without`, or `overdue`, and only account-wide. `basecamp todos list --due tomorrow` is rejected. For date-based listing use `--overdue`, `--no-due-date`, or `basecamp assignments due <scope>`
    - Project from `.basecamp/config.json` if `--in` not specified
    - Multiple identities use named profiles: `basecamp profile create <name>`, then select one with global `--profile <name>` or `BASECAMP_PROFILE=<name>`.
    
    ## Quick Reference
    
    > **Note:** Most queries require project scope (via `--in <project>` or `.basecamp/config.json`). Cross-project exceptions: `basecamp reports assigned`, `basecamp assignments`, `basecamp reports overdue`, `basecamp reports schedule`, `basecamp recordings <type>`, `basecamp notifications`, `basecamp gauges list`, `basecamp events poll`, `basecamp inbox`.
    >
    > Seven list commands also list account-wide: `basecamp todos list --all-projects --json`, and likewise `cards list`, `messages list`, `comments list`, `files list`, `forwards list`, and `checkins answers`.
    
    | Task | Command |
    |------|---------|
    | List projects | `basecamp projects list --json` |
    | My todos (in project) | `basecamp todos list --assignee me --in <project> --json` |
    | My todos (cross-project) | `basecamp reports assigned --json` (defaults to "me") |
    | My schedule (cross-project) | `basecamp reports schedule --json` (upcoming events across all projects) |
    | All todos (cross-project) | `basecamp todos list --all-projects --json` (grouped by project) |
    | Overdue todos (in project) | `basecamp todos list --overdue --in <project> --json` |
    | Overdue todos (cross-project) | `basecamp todos list --all-projects --overdue --json` (flat, oldest first) or `basecamp reports overdue --json` (bucketed by lateness) |
    | All cards (cross-project) | `basecamp cards list --all-projects --json` (grouped by project) |
    | Someone's todos (cross-project) | `basecamp todos list --all-projects --assignee "Ann" --json` (server-side filter) |
    | Two people's todos (cross-project) | `basecamp todos list --all-projects --assignee ann --assignee bob --json` (matches either) |
    | Someone's cards (cross-project) | `basecamp cards list --all-projects --assignee "Ann" --json` |
    | Todos with no due date set (cross-project) | `basecamp todos list --all-projects --due without --json` |
    | My bookmarks | `basecamp bookmarks list --json` |
    | Bookmark something | `basecamp bookmarks add <id-or-url> --json` |
    | Is it bookmarked? | `basecamp bookmarks check <id-or-url> --json` (always exits 0) |
    | Bubble a recording up | `basecamp bubble-up add <id-or-url> --json` |
    | Schedule a bubble-up | `basecamp bubble-up add <id-or-url> --at tomorrow --json` |
    | Pop a bubble-up | `basecamp bubble-up remove <id-or-url> --json` |
    | My unpublished drafts | `basecamp drafts list --json` |
    | Read my personal note | `basecamp notes show --json` |
    | Replace my personal note | `basecamp notes set "<content>" --json` |
    | Check-ins I owe answers to | `basecamp checkins reminders --json` |
    | Add to Up Next | `basecamp assignments prioritize <id> --json` |
    | Recolor a calendar | `basecamp calendars update <id-or-url> --color blue --json` |
    | Todo outside any list | `basecamp todos create "<content>" --loose --in <project> --json` |
    | Assign todo | `basecamp assign <id> [id...] --to <person> --in <project> --json` |
    | Assign card | `basecamp assign <id> [id...] --card --to <person> --in <project> --json` |
    | Assign card step | `basecamp assign <id> [id...] --step --to <person> --in <project> --json` |
    | Create todo | `basecamp todos create "Task" --in <project> --list <list> --json` |
    | Create todolist | `basecamp todolists create "Name" --in <project> --json` |
    | Complete todo | `basecamp todos complete <id> --json` |
    | List cards | `basecamp cards list --in <project> --json` |
    | Create card | `basecamp cards create "Title" --in <project> --json` |
    | Complete card | `basecamp cards done <id|url> --in <project> --json` |
    | Move card | `basecamp cards move <id> --to <column> [--position N] --in <project> --json` |
    | Move card to on-hold | `basecamp cards move <id> --on-hold --in <project> --json` |
    | Move card to another project | `basecamp cards move <id> --to-wormhole <wormhole_id> --in <project> --json` (async teleport) |
    | Post message | `basecamp messages create "Title" "Body" --in <project> --json` |
    | Post with @mention | `basecamp messages create "Title" "Hey @First.Last, ..." --in <project> --json` |
    | Post silently | `basecamp messages create "Title" "Body" --no-subscribe --in <project> --json` |
    | Post to chat | `basecamp chat post "Message" --in <project> --json` |
    | List pings | `basecamp notifications --json --jq '.data.reads[]? | select(.section == "pings")'` |
    | Read ping thread | `basecamp api get "/buckets/<circle_id>/chats/<chat_id>/lines.json" --agent` |
    | Post to ping thread | `basecamp api post "/buckets/<circle_id>/chats/<chat_id>/lines.json" --data '{"content":"<p>message</p>"}' --json` |
    | Add comment | `basecamp comments create <recording_id> "Text" --in <project> --json` |
    | Inspect comment / reply atoms | `basecamp comments show <url> --json` → `reply_target` + `mention` in `.data` |
    | List attachments | `basecamp attachments list <id\|url> --json` |
    | Download attachments | `basecamp attachments download <id> --out /tmp/` |
    | Show + download | `basecamp todos show <id> --download-attachments --json` |
    | Stream attachment to stdout | `basecamp attachments download <id> --file <name> --out -` |
    | Change history for an item | `basecamp events <id\|url> --json` (when a card moved columns, when a todo was completed) |
    | Account-wide activity feed (resumable) | `basecamp events poll --since now --json` (then resume with `--position`) |
    | Items that addressed me (agents only) | `basecamp inbox --since now --json` |
    | Search | `basecamp search "query" --json` |
    | Parse URL | `basecamp url parse "<url>" --json` |
    | Upload file | `basecamp files uploads create <file> [--vault <folder_id>] --in <project> --json` |
    | Download file | `basecamp files download <id> --in <project>` |
    | Stream file to stdout | `basecamp files download <id> --out - --in <project>` |
    | Download storage URL | `basecamp files download "https://storage.3.basecamp.com/.../download/report.pdf"` |
    | My assignments | `basecamp assignments --json` (priorities + non-priorities) |
    | Overdue assignments | `basecamp assignments due overdue --json` |
    | Completed assignments | `basecamp assignments completed --json` |
    | Notifications | `basecamp notifications --json` |
    | Mark notification read | `basecamp notifications read <id> --json` |
    | All bubble-ups (BC5) | `basecamp notifications bubbleups --json` |
    | Gauges (account-wide) | `basecamp gauges list --json` |
    | Gauge needles | `basecamp gauges needles --in <project> --json` |
    | Create needle | `basecamp gauges create --position 75 --color green --in <project> --json` |
    | Account details | `basecamp accounts show --json` |
    
    ## URL Parsing
    
    **Parse URLs before acting on them — unless you're handing the URL to a command
    that accepts a URL directly** (`show`, `comments show`, `comments thread`,
    `attachments list`/`attachments download`), which extract the IDs for you. Only `comments show` and
    `comments thread` verify the URL's host and account before any fetch. For other
    URL-accepting commands, only pass URLs from a trusted Basecamp host:
    `basecamp url parse` extracts IDs but does **not** validate the URL's origin, so
    parsing an attacker-controlled path yields trusted-looking IDs.
    
    ```bash
    basecamp url parse "https://3.basecamp.com/2914079/buckets/41746046/messages/9478142982#__recording_9488783598" --json
    ```
    
    Returns: `account_id`, `project_id`, `type`, `recording_id`, `comment_id` (from fragment).
    
    **URL patterns:**
    - `/buckets/27/messages/123` - Message 123 in project 27
    - `/buckets/27/messages/123#__recording_456` - Comment 456 on message 123
    - `/buckets/27/card_tables/cards/789` - Card 789
    - `/buckets/27/card_tables/columns/456` - Column 456 (for creating cards)
    - `/buckets/27/todos/101` - Todo 101
    - `/buckets/27/uploads/202` - Upload/file 202
    - `/buckets/27/documents/303` - Document 303
    - `/buckets/27/schedule_entries/404` - Schedule entry 404
    
    **Replying to comments:**
    ```bash
    # Comments are flat - reply to the parent recording_id, not the comment_id
    basecamp url parse "https://...messages/123#__recording_456" --json
    # Returns recording_id: 123 (parent), comment_id: 456 (fragment) - comment on 123, not 456
    basecamp comments create 123 "Reply" --in <project>
    
    # Or get the whole reply-ready context deterministically in one call:
    basecamp comments thread "https://...messages/123#__recording_456" --json
    # .data.reply_target.recording_id  → where to post the reply
    # .data.reply_target.account_id    → the account that reply belongs to (build a fully-qualified command)
    # .data.focus.author.mention.syntax → paste-ready [@Name](mention:SGID)
    # .data.comments                   → surrounding discussion (default window of 41)
    # --all returns every fetched comment; --window N sets the window size
    # When the account came from the URL (none configured), the reply breadcrumb carries --account
    ```
    
    ## Decision Trees
    
    ### Finding Content
    
    ```
    Need to find something?
    ├── Know the type + project? → basecamp <type> list --in <project> --json
    │   (some groups have default list behavior; use --agent --help if unsure)
    ├── My assigned work? → basecamp assignments --json (priorities + non-priorities)
    │   Or: basecamp reports assigned --json (traditional view, defaults to "me")
    ├── My overdue assignments? → basecamp assignments due overdue --json
    ├── My notifications? → basecamp notifications --json
    ├── Upcoming schedule? → basecamp reports schedule --json (cross-project)
    ├── Overdue across projects? → basecamp reports overdue --json
    ├── Browse by type cross-project? → basecamp recordings <type> --json
    │   (types: todos, messages, documents, comments, cards, uploads)
    │   Note: Defaults to active status; use --status archived for archived items
    │   ⚠ No assignee data — cannot filter by person; use reports assigned instead
    ├── Full-text search? → basecamp search "query" --json
    ├── Have a comment URL, or a notification link targeting a comment? → basecamp comments thread <url> --json
    └── Have a URL? → basecamp url parse "<url>" --json
    ```
    
    ### Modifying Content
    
    ```
    Want to change something?
    ├── Have URL? → basecamp url parse "<url>" → use extracted IDs
    ├── Have ID? → basecamp <resource> update <id> --field value
    ├── Change status? → basecamp recordings trash|archive|restore <id>
    ├── Complete todo? → basecamp todos complete <id>
    ├── Complete card? → basecamp cards done <id|url> --in <project>
    └── Reply to a comment? → basecamp comments show <url> --jq '.data | {reply_target, mention}'
        (one call, cheap atoms — the mention is machine-only, so use --jq/--json, not plain show)
        or basecamp comments thread <url> when you need the surrounding discussion;
        then basecamp comments create <reply_target.recording_id> <text>
    ```
    
    ## Common Workflows
    
    ### Link Code to Basecamp Todo
    
    ```bash
    # Get commit info and comment on todo (use printf %q for safe quoting)
    COMMIT=$(git rev-parse --short HEAD)
    MSG=$(git log -1 --format=%s)
    basecamp comments create <todo_id> "Commit $COMMIT: $(printf '%s' "$MSG")" --in <project>
    
    # Complete when done
    basecamp todos complete <todo_id>
    ```
    
    ### Track PR in Basecamp
    
    ```bash
    # Create todo for PR work
    basecamp todos create "Review PR #42" --in <project> --assignee me --due tomorrow
    
    # When merged
    basecamp todos complete <todo_id>
    basecamp chat post "Merged PR #42" --in <project>
    ```
    
    ### Bulk Process Overdue Todos
    
    ```bash
    # Preview overdue todos
    basecamp todos sweep --overdue --dry-run --in <project>
    
    # Complete all with comment
    basecamp todos sweep --overdue --complete --comment "Cleaning up" --in <project>
    ```
    
    ### Mentioning people (preferred — deterministic)
    
    ```bash
    # 1. Look up the person
    basecamp people pingable --jq '.data[] | select(.name == "Jane Smith")'
    # => {"id": 42000, "attachable_sgid": "BAh7CEkiCG...", "name": "Jane Smith"}
    
    # 2. Use SGID in Markdown mention syntax (zero API calls during post)
    basecamp comments create 123 "Hey [@Jane Smith](mention:BAh7CEkiCG...), check this" --in <project>
    
    # Or use person ID (one lookup during post)
    basecamp comments create 123 "Hey [@Jane Smith](person:42000), check this" --in <project>
    ```
    
    ### Mentioning people (interactive — may be ambiguous)
    
    ```bash
    # Fuzzy matching: use @First.Last to reduce ambiguity
    basecamp comments create <id> "@Jane.Smith, please review this" --in <project>
    basecamp messages create "Update" "cc @Jane, @Alex" --in <project>
    basecamp chat post "@Jane, done!" --in <project>
    
    # Ambiguous names return an error with suggestions
    # Use @First.Last for disambiguation
    ```
    
    ### Move Card Through Workflow
    
    ```bash
    # List columns to get IDs
    basecamp cards columns --in <project> --json
    
    # Complete a card (moves it to the Done column automatically)
    basecamp cards done <card_id> --in <project>
    
    # Move card to column
    basecamp cards move <card_id> --to <column_id> --in <project>
    
    # Move card to specific position in column (1-indexed)
    basecamp cards move <card_id> --to <column_id> --position 1 --in <project>
    
    # Move card to on-hold section of its current column
    basecamp cards move <card_id> --on-hold --in <project>
    
    # Move card to on-hold section of a specific column (numeric ID)
    basecamp cards move <card_id> --to <column_id> --on-hold --in <project>
    
    # Move card to on-hold section of a named column (requires --card-table)
    basecamp cards move <card_id> --to "Column Name" --on-hold --card-table <table_id> --in <project>
    ```
    
    ### Download File from Basecamp
    
    ```bash
    basecamp files download <upload_id> --in <project> --out ./downloads
    
    # Download attachment from a storage URL (no --in needed)
    basecamp files download "https://storage.3.basecamp.com/123/blobs/abc/download/report.pdf"
    
    # Stream to stdout (for piping)
    basecamp files download <upload_id> --out - --in <project>
    ```
    
    ### Working with Attachments (Multimodal Agent Workflow)
    
    Messages, todos, cards, and documents may contain images and file attachments
    (mockups, screenshots, annotated designs). Show commands surface these as
    field-scoped collections — `content_attachments` and/or `description_attachments`
    — keyed by which rich-text attribute contained them. The notice field hints at
    the download command.
    
    **Step 1: Fetch the recording and check for attachments**
    ```bash
    basecamp todos show <id> --json
    # Response includes description_attachments when attachments are present
    # Messages/documents use content_attachments; cards may have both
    # The notice field hints: "3 attachment(s) — download: basecamp attachments download <id>"
    ```
    
    **Step 2 (one-shot): Download attachments with the show command**
    ```bash
    # --download-attachments fetches + downloads in one shot
    basecamp todos show <id> --download-attachments --json
    # content_attachments/description_attachments entries now include "path" pointing to local files
    # Downloads to OS temp dir by default, or specify: --download-attachments /tmp/att
    ```
    
    **Step 2 (two-step alternative): Download separately**
    ```bash
    # Download all at once (shows progress on stderr)
    basecamp attachments download <id> --out /tmp/attachments
    ```
    
    **Step 3: View images with your native file-read tool**
    For multimodal LLMs (Claude, Gemini), use your file-read tool on the `path`
    from the response to view downloaded images directly — no browser needed.
    This surfaces visual context (mockups, screenshots, annotated designs) that
    is often the most important part of a Basecamp todo or message.
    
    ```bash
    # Stream a single image to stdout for piping
    basecamp attachments download <id> --file mockup.png --out -
    
    # Select by index when names collide
    basecamp attachments download <id> --index 2 --out -
    ```
    
    **Key pattern:** When a show command response contains `content_attachments`
    or `description_attachments`, always download and view them — visual context is
    often more important than the text content. Use `--download-attachments` for
    one-shot fetch+download, or follow the breadcrumb hint for two-step control.
    
    ## Resource Reference
    
    ### Projects
    
    ```bash
    basecamp projects list --json               # List all
    basecamp projects show <id> --json          # Show details
    basecamp projects create "Name" --json      # Create
    basecamp projects update <id> --name "New"  # Update
    basecamp projects trash <id>                # Move to trash (recoverable)
    ```
    
    **Archiving a project:** the CLI does not have a dedicated archive command, but the
    underlying status endpoint can be hit via raw API. Same path works for restoring
    to active or moving to trashed.
    
    ```bash
    basecamp api put "projects/<id>/status/archived" -d '{}' --json   # Archive
    basecamp api put "projects/<id>/status/active" -d '{}' --json     # Unarchive
    basecamp api put "projects/<id>/status/trashed" -d '{}' --json    # Trash (same as `projects trash`)
    ```
    
    Verify with `basecamp projects show <id> --jq '.data.status'`.
    
    ### Todos
    
    ```bash
    basecamp todos list --in <project> --json               # List in project
    basecamp todos list --assignee me --in <project>        # My todos
    basecamp todos list --overdue --in <project>            # Overdue only
    basecamp todos list --status completed --in <project>   # Completed
    basecamp todos list --list <todolist_id> --in <project> # In specific list
    basecamp todos create "Task" --in <project> --list <list> --assignee me --due tomorrow
    basecamp todos complete <id> [id...]                    # Complete (multiple OK)
    basecamp todos uncomplete <id>                          # Reopen
    basecamp assign <id> [id...] --to <person> --in <project>       # Assign to-do (multiple OK)
    basecamp unassign <id> [id...] --from <person> --in <project>   # Remove to-do assignee (multiple OK)
    basecamp assign <id> [id...] --card --to <person> --in <project>   # Assign card
    basecamp unassign <id> [id...] --card --from <person> --in <project> # Remove card assignee
    basecamp assign <id> [id...] --step --to <person> --in <project>   # Assign card step
    basecamp unassign <id> [id...] --step --from <person> --in <project> # Remove step assignee
    basecamp todos position <id> --to 1                     # Move to top
    basecamp todos position <id> --to 1 --list <id|name|url> # Move to different list
    basecamp todos sweep --overdue --complete --comment "Done" --in <project>
    basecamp todos create "Task" --in <project> --list <list> --notify-on-completion "Jane,Bob"  # Notify when done
    basecamp todos update <id> --notify-on-completion "Jane"  # Set who's notified on completion
    basecamp todos update <id> --no-notify-on-completion      # Clear completion notifications
    ```
    
    **Flags:** `--assignee` (repeatable; server-side account-wide, client-side within a project; also on `cards list` account-wide, but not on messages), `--status` (completed/incomplete/archived/trashed), `--overdue`, `--list`, `--due` (**listing filter: `with`/`without`/`overdue` only, account-wide only** — not a date; see Smart Defaults), `--limit`, `--all`
    
    **Completion subscribers** ("When done, notify…"): set with
    `--notify-on-completion <names or IDs, comma-separated>` on `todos create` and
    `todos update`; clear with `--no-notify-on-completion` on `todos update`.
    Plain updates (title, due date, etc.) preserve existing completion subscribers.
    
    **Todo Subtasks (checklist steps):** Basecamp to-do subtasks are stored as
    `Kanban::Step` records, even when their parent is a normal `Todo`. The regular
    `basecamp todos show` response may not include them; use
    `basecamp recordings list --in <project> --type Kanban::Step` and filter by
    `parent.id` to list/check subtasks for a todo.
    
    ```bash
    # Create a subtask under a todo.
    # Use the numeric project ID and todo ID in this card-style path.
    basecamp api post /buckets/<project_id>/card_tables/cards/<parent_todo_id>/steps.json \
      --data '{"title":"Subtask title"}' \
      --json
    
    # Read or edit a subtask
    basecamp api get /buckets/<project_id>/card_tables/steps/<step_id>.json --json
    basecamp api put /buckets/<project_id>/card_tables/steps/<step_id>.json \
      --data '{"title":"Updated subtask title"}' \
      --json
    
    # List subtasks for a todo
    PARENT_TODO_ID=<parent_todo_id> \
    basecamp recordings list --in <project> --type Kanban::Step --all \
      --jq '.data[] | select(.parent.id==(env.PARENT_TODO_ID | tonumber)) | {id,title,status,parent:.parent.id,url}'
    
    # Assign or set a due date. Send only what you're changing — omitted fields are
    # left alone. `assignee_ids` replaces the whole list, so name everyone who stays.
    basecamp api put /buckets/<project_id>/card_tables/steps/<step_id>.json \
      --data '{"assignee_ids":[<person_id>,<existing_person_id>],"due_on":"<YYYY-MM-DD>"}' \
      --json
    
    # Complete or reopen a subtask
    basecamp api put /buckets/<project_id>/card_tables/steps/<step_id>/completions.json \
      --data '{"completion":"on"}' \
      --json
    basecamp api put /buckets/<project_id>/card_tables/steps/<step_id>/completions.json \
      --data '{"completion":"off"}' \
      --json
    
    # Trash a subtask from the todo UI by trashing the step record (Kanban::Step)
    basecamp recordings trash <step_id> --in <project> --json
    ```
    
    Key points: replace numeric placeholders such as `<project_id>`,
    `<parent_todo_id>`, and `<person_id>` before running the examples. Bucket-scoped
    API paths require a numeric project/bucket ID; `--in <project>` can still accept
    a project name where CLI commands support name resolution. For creating todo
    subtasks, Basecamp accepts the parent todo ID in the
    `/buckets/<project_id>/card_tables/cards/<parent_todo_id>/steps.json` path. To
    list subtasks under a todo, use
    `basecamp recordings list --in <project> --type Kanban::Step` with the
    `parent.id` filter shown above.
    
    Completed subtasks have `completed: true` and a `completion` object with
    `created_at` and `creator`. Open subtasks have `completed: false` and no
    `completion` object. Trashed subtasks may still be readable directly with
    `status: "trashed"` and `inherits_status: false`, but they no longer appear in
    the todo UI.
    
    In testing with todo-backed steps, these bucket-scoped direct `GET` requests
    returned `not_found`:
    `/buckets/<project_id>/card_tables/cards/<parent_todo_id>/steps.json`,
    `/buckets/<project_id>/card_tables/cards/<parent_todo_id>.json`, and
    `/buckets/<project_id>/todos/<parent_todo_id>/steps.json`. To inspect trashed
    subtasks, add `--status trashed`; archived parents may require
    `--status archived`.
    
    **Raw step updates are partial.** `PUT .../card_tables/steps/<id>.json` leaves
    every parameter you omit unchanged, so send only the fields you are changing.
    Echoing back a `title` you did not mean to change is not merely redundant — it
    reverts anyone who edited the title between your read and your write. To clear a
    value, say so explicitly: `"due_on": null` clears the due date, `"assignee_ids":
    []` removes everyone. `assignee_ids` always replaces the whole list rather than
    adding to it, so name every person who should remain assigned.
    
    (This is bc3#12521. Before it, an omitted field *was* cleared and a title-less
    update was rejected, which is why older guidance said to resend the title. Todo
    subtasks and card steps share one endpoint and one contract — `PUT
    card_tables/steps/:id` routes to the same controller for both.)
    
    The generic
    `basecamp assign <step_id> --step ...` command is intended for card steps and
    may fail with `Bad Request` for todo-backed steps, so prefer `assignee_ids` on
    the raw step update endpoint for todo subtasks.
    
    ### Todolists
    
    Todolists are containers for todos. Create a todolist before adding todos.
    
    ```bash
    basecamp todolists list --in <project> --json              # List todolists
    basecamp todolists show <id> --in <project>                # Show details
    basecamp todolists create "Name" --in <project> --json     # Create
    basecamp todolists create "Name" --description "Desc" --in <project>
    basecamp todolists create "Name" --visible-to-clients --in <project>  # Visible to clients
    basecamp todolists update <id> --name "New" --in <project> # Update
    basecamp todolists position <id> --to 1                     # Reorder one list (1 = top)
    basecamp todolists position <id> <id> <id>                  # Order incomplete lists, top→bottom
    ```
    
    Bulk `position` sets the visible order in one command: pass incomplete lists from
    the same todoset, top to bottom. It always places them at the top.
    
    ### Cards (Kanban)
    
    **Note:** `--assignee` on `cards list` is **account-wide only** — pass `--all-projects` (or have no project in scope) and it becomes a real server-side filter. Within a single project cards have no assignee filter: fetch all and filter client-side. `--due with|without|overdue` is account-wide only on cards too. If a project has multiple card tables, you must specify `--card-table <id>`. When you get an "Ambiguous card table" error, the hint shows available table IDs and names.
    
    ```bash
    basecamp cards list --in <project> --json             # All cards
    basecamp cards list --card-table <id> --in <project>  # Specific table (required if multiple)
    basecamp cards list --column <id> --in <project>      # Cards in column
    basecamp cards columns --in <project> --json          # List columns (needs --card-table if multiple)
    basecamp cards show <id> --in <project>               # Card details
    basecamp cards create "Title" "<p>Body</p>" --in <project> --column <id>
    basecamp cards update <id> --title "New" --due tomorrow --assignee me
    basecamp cards done <id|url> --in <project>           # Move to the Done column automatically
    basecamp cards move <id> --to <column_id>             # Move to column (numeric ID)
    basecamp cards move <id> --to "Done" --card-table <table_id>  # Move by name (needs table)
    basecamp cards move <id> --to "Done" --position 1 --card-table <table_id>  # Move to position
    basecamp cards move <id> --on-hold                    # Move to on-hold of current column
    basecamp cards move <id> --to <column_id> --on-hold   # Move to on-hold of target column
    ```
    
    **Cross-project card move (wormholes):** the only way to move a card to another
    project is to teleport it through a *wormhole* — a portal on the card table that
    sends cards to a preconfigured column on another project's card table (max 4 per
    table). The teleport is **asynchronous and mints a new card id**: after the move
    is accepted, the server copies the card into the destination and deletes the
    original, so the **original id 404s** — do not reuse it.
    
    ```bash
    basecamp cards wormholes list --in <project>          # Discover wormholes (id, destination, linked)
    basecamp cards wormholes create --to-column <id|url> --in <project>   # Link to a column on another table (≤4)
    basecamp cards wormholes update <id> --to-column <id|url> --in <project>
    basecamp cards wormholes delete <id> --in <project>
    basecamp cards move <card_id> --to-wormhole <wormhole_id> --in <project>          # Teleport (async)
    basecamp cards move <card_id> --to-wormhole <destination_column_url> --in <project>  # Match by destination column
    ```
    
    `--to-wormhole` is mutually exclusive with `--to`/`--on-hold`/`--position`. Pass
    a numeric wormhole id to route directly, or a destination-column URL to match it
    against the source table's wormholes.
    
    **Archived/trashed cards:** `cards list` only returns active cards. For archived or trashed cards, use `basecamp recordings cards --status archived --in <project>` or `--status trashed`.
    
    **Identifying completed cards:** Cards in Done columns have `parent.type: "Kanban::DoneColumn"` and `completed: true`. Use this to identify completed cards that haven't been archived.
    
    **When a card moved columns:** don't read `updated_at` — it changes on any
    modification. Use the event history instead: `basecamp events <card_id> --json`
    records an `adopted` event for every column move, and a card crossing into or
    out of a Done column pairs that with `completed`/`uncompleted`. See
    [Events](#events-change-history).
    
    **Card Steps (checklists):**
    ```bash
    basecamp cards steps <card_id> --in <project>     # List steps
    basecamp cards step create "Step" --card <id> --in <project>
    basecamp cards step complete <step_id> --in <project>
    basecamp cards step uncomplete <step_id>
    ```
    
    **Column management:**
    ```bash
    basecamp cards column show <id> --in <project>
    basecamp cards column create "Name" --in <project>
    basecamp cards column update <id> --title "New"
    basecamp cards column move <id> --position 2
    basecamp cards column color <id> --color blue
    basecamp cards column on-hold <id>                # Enable on-hold section
    basecamp cards column watch <id>                  # Subscribe to column
    ```
    
    ### Messages
    
    ```bash
    basecamp messages list --in <project> --json  # List messages
    basecamp messages show <id> --in <project>    # Show message
    basecamp messages create "Title" "Body" --in <project>
    basecamp messages create "Draft" "WIP" --draft --in <project>  # Create draft
    basecamp messages publish <id>               # Publish a draft
    basecamp messages update <id> --title "New" --body "Updated"
    basecamp messages pin <id> --in <project>     # Pin to top
    basecamp messages unpin <id>                  # Unpin
    ```
    
    **Archived/trashed messages:** `messages list` only returns active messages. For archived or trashed messages, use `basecamp recordings messages --status archived --in <project>` or `--status trashed`.
    
    **Flags:** `--draft` (create as draft), `--no-subscribe` (silent, no notifications), `--subscribe "people"` (comma-separated names, emails, IDs, or "me"; mutually exclusive with `--no-subscribe`), `--message-board <id>` (if multiple boards), `--visible-to-clients` (make visible to clients on the project; omit for the server default)
    
    ```bash
    basecamp messages create "Bot update" "Done" --no-subscribe --in <project>
    basecamp messages create "FYI" "Note" --subscribe "Alice,bob@x.com" --in <project>
    basecamp messages create "For the client" "..." --visible-to-clients --in <project>
    ```
    
    **Client visibility at create time:** `messages create`, `todolists create`,
    `schedule create`, `checkins question create`, and `tools create` accept
    `--visible-to-clients` to post a client-visible recording in one call (for
    `tools create`, only chat and kanban_board tool types honor it — other types
    inherit the project default). Omitting the flag uses the
    server default, which is context-dependent: **team-only when you post as a team
    member**, but a **client-authenticated caller always creates client-visible
    records** (an explicit `--visible-to-clients=false` is overridden server-side for
    client callers). Passing `--visible-to-clients` posts client-visible in every
    case. To change visibility on an already-created recording, use
    `recordings visibility <id> --visible`.
    
    ### Comments
    
    ```bash
    basecamp comments list <recording_id> --in <project> --json
    basecamp comments show <comment-id|comment-url> --json            # Now returns reply_target + paste-ready mention (JSON)
    basecamp comments thread <comment-id|comment-url> --json          # Reply-ready: parent + focus + discussion + @mention
    basecamp comments thread <comment-id> --all --json                # Every fetched comment instead of a window
    basecamp comments thread <comment-id> --window 11 --json          # Focus-centered window of 11
    basecamp comments create <recording_id> "Text" --in <project>
    basecamp comments create <recording_id> "@Jane.Smith, looks good!" --in <project>  # With @mention
    basecamp comments update <id> "Updated" --in <project>
    ```
    
    **Cheap atoms vs. deep context (choose by need):**
    - `comments show <url> --jq '.data | {reply_target, mention}'` — one API call. Returns
      `reply_target` (`recording_id` — where a reply is posted, comments are flat — plus
      `account_id`) and a paste-ready author `mention` (JSON only; human output shows a reply
      breadcrumb). Use this for the exact-comment reply atoms.
    - `comments thread <url>` — two extra calls. Adds the full parent recording, the
      surrounding discussion (windowed, truncation-honest), and focus attachments. Use this
      when the surrounding discussion matters.
    
    ### Files & Documents
    
    ```bash
    basecamp files list --in <project> --json               # List all (folders, files, docs)
    basecamp files list --vault <folder_id> --in <project>  # List folder contents
    basecamp files list --all-projects --json               # Across every project (first 100)
    basecamp files list --all-projects --limit 500          # Walk pages until 500 collected
    basecamp files list --all-projects --page 2             # Exactly page 2
    basecamp files list --all-projects --all                # Every page (slow on big accounts)
    basecamp files show <id> --in <project>                 # Show item (auto-detects type)
    basecamp files versions <upload_id> --json              # Every version of an uploaded file
    basecamp files versions <upload_id> --limit 5 --json    # Cap results (default: all)
    basecamp files replace <upload_id> <file>               # Replace the file, keep the ID/URL/comments
    basecamp files replace <upload_id> <file> --description "v2 notes"  # Also set a new description
    basecamp files download <id> --in <project>             # Download file
    basecamp files download <id> --out ./dir                # Download to specific dir
    basecamp files download "https://storage.../download/f" # Download from storage URL
    basecamp files uploads create <file> --in <project>      # Upload file to root
    basecamp files uploads create <file> --vault <folder_id> --in <project>  # Upload to folder
    basecamp files uploads create <file> --visible-to-clients --in <project>  # Client-visible (root folder only)
    basecamp files folder create "Folder" --in <project>
    basecamp files doc create "Doc" "Body" --in <project>
    basecamp files doc create "Draft" --draft --in <project>
    basecamp files doc create "Notes" "..." --no-subscribe --in <project>
    basecamp files doc create "For client" "..." --visible-to-clients --in <project>  # Client-visible (root folder only)
    basecamp files update <document_id> --title "New" --content "Updated"
    basecamp files update <document_id> --title "New" --in <project>      # Preserves existing document content
    basecamp files update <document_id> --content "Updated" --in <project> # Preserves existing document title
    ```
    
    **Document update semantics:** `basecamp files update <document_id>` is safe for partial updates in the CLI: when you pass only `--title` or only `--content`, the CLI first fetches the current document and preserves the untouched field.
    
    **Client visibility at create time:** `doc create` and `uploads create` accept
    `--visible-to-clients`, but the server only honors it in the project's **root
    Docs & Files folder**. Targeting a nested folder (`--vault`/`--folder`) with the
    flag is a hard error raised before anything is uploaded — a nested item inherits
    its folder's visibility, and that can't be changed per-item afterward (the
    visibility endpoint rejects nested docs/uploads). To make a nested item
    client-visible, create it in the root folder, or change the eligible top-level
    ancestor that controls the folder's visibility first. Omitting the flag uses the
    server default; as with Messages, a **client-authenticated caller always creates
    client-visible records** regardless. `recordings visibility` is **not** a
    remediation for nested docs/uploads.
    
    **Upload versions:** replacing a file keeps the earlier copies under the same
    upload ID, so `basecamp files versions <upload_id>` is how you see the history of
    one file. A file that was never replaced returns its single current version, not
    an error. Only `--page 1` is accepted; use `--all` to walk every page.
    `basecamp files replace <upload_id> <file>` publishes a new version in place —
    the upload keeps its ID, URL and comments, nobody is notified, and the
    description carries forward unless `--description` is given. Use it instead of
    `uploads create` when shipping a new build of the same file.
    
    **Subcommands:** `folders`, `uploads`, `documents` (each with pagination flags)
    
    ### Schedule
    
    For upcoming events across all projects, use `basecamp reports schedule --json`.
    
    ```bash
    basecamp schedule info --in <project> --json       # Schedule info
    basecamp schedule entries --in <project> --json   # List entries
    basecamp schedule show <id> --in <project>        # Entry details
    basecamp schedule show <id> --date 20240315       # Specific occurrence (recurring)
    basecamp schedule create "Event" --starts-at "2024-03-15T09:00:00Z" --ends-at "2024-03-15T10:00:00Z" --in <project>
    basecamp schedule create "Meeting" --all-day --notify --participants 1,2,3 --in <project>
    basecamp schedule create "Sync" --starts-at "..." --ends-at "..." --no-subscribe --in <project>
    basecamp schedule update <id> --summary "New title" --starts-at "..."
    basecamp schedule settings --include-due --in <project>  # Include todos/cards due dates
    ```
    
    **Flags:** `--all-day`, `--notify`, `--participants <ids>`, `--no-subscribe`, `--subscribe "people"` (mutually exclusive), `--status` (active/archived/trashed), `--visible-to-clients` (make visible to clients; omit for the server default)
    
    ### Check-ins
    
    ```bash
    basecamp checkins --in <project> --json           # Questionnaire info
    basecamp checkins questions --in <project>        # List questions
    basecamp checkins question <id> --in <project>    # Question details
    basecamp checkins answers <question_id> --in <project>  # List answers
    basecamp checkins answers <question_id> --by me --in <project>  # My answers only
    basecamp checkins answers <question_id> --by "Alice Smith" --in <project>  # Filter by person (name, email, or ID)
    basecamp checkins answer <id> --in <project>      # Answer details
    basecamp checkins question create "What did you work on?" --in <project>
    basecamp checkins question update <id> "New question" --frequency every_week
    basecamp checkins answer create <question-id> "My answer" --in <project>  # Defaults to today
    basecamp checkins answer update <id> "Updated" --in <project>
    ```
    
    **Schedule options:** `--frequency` (every_day, every_week, every_other_week, every_month, on_certain_days), `--days 1,2,3,4,5` (0=Sun), `--time "5:00pm"`
    
    **Client visibility:** `checkins question create` accepts `--visible-to-clients` to make the question visible to clients (omit for the server default; see the note under Messages for the context-dependent rule).
    
    **Managing a question:**
    
    ```bash
    basecamp checkins question pause <id> --json      # Stop asking it
    basecamp checkins question resume <id> --json     # Start asking it again
    basecamp checkins question answerers <id> --json  # Who answers it
    basecamp checkins question notify <id> --on-answer --json
    basecamp checkins question notify <id> --no-on-answer --json
    basecamp checkins question notify <id> --digest-include-unanswered --json
    ```
    
    `notify` changes **your own** settings, and each one is left alone unless you
    name it — so `--on-answer` does not silently reset the digest setting. The
    `--no-...` spellings send an explicit false; passing neither setting is refused
    rather than sent as an empty update.
    
    **Your pending reminders** (account-wide, no `--in`):
    
    ```bash
    basecamp checkins reminders --json
    basecamp checkins reminders --limit 10 --json
    ```
    
    `reminders` and `answerers` take `--limit` but deliberately **no `--page`**: the
    API does not honor a page number on these, so the flag would accept a value it
    could not act on.
    
    ### Timeline
    
    ```bash
    basecamp timeline --json                          # Account-wide activity
    basecamp timeline --in <project> --json           # Project activity
    basecamp timeline me --json                       # Your activity
    basecamp timeline --person <id> --json            # Person's activity
    ```
    
    Use `--limit N` to cap results or `--all` to fetch everything (default: 100 events).
    
    ### Events (change history)
    
    `basecamp timeline` reports activity across a project or account. For the audit
    trail of one specific item — todo, card, message, document — use `basecamp
    events`:
    
    ```bash
    basecamp events <id|url> --json                   # Change history for one item
    basecamp events <id> --limit 25 --json            # Cap results (default 100)
    basecamp events <id> --all --json                 # Fetch everything
    ```
    
    Common `action` values: `created`, `completed`/`uncompleted`,
    `assignment_changed`, `content_changed`, `archived`/`unarchived`,
    `commented_on`, and — for cards — `adopted`, which is recorded every time a card
    moves to another column. That makes `events` the way to answer "when did this
    card move?" or "when was this actually finished?", neither of which `updated_at`
    can tell you.
    
    `--page` accepts only `1`; use `--all` to walk every page.
    
    ### Event feed (account-wide, resumable)
    
    `basecamp events <id>` is one recording's history. The account-wide **event
    feed** is a different resource, and the way an agent hears about activity it
    did not cause:
    
    ```bash
    basecamp events poll --since now --json            # Enter at the present
    basecamp events poll --position "$POSITION" --json # Resume from a held position
    basecamp events poll --since 0 --all --json        # Replay served history
    basecamp inbox --since now --json                  # Addressed items (agents only)
    basecamp events ticket --json                      # Mint a live-stream ticket
    ```
    
    Each page is an envelope: `events` (or `items`), a durable `position`, and a
    `next` continuation URL while the current walk has more to serve. Persist
    `position` only after processing the page, then pass it back with
    `--position`. The response's `notice` spells the whole resume command out,
    filters included. `--all` walks `next` to the end of the current walk — it is
    not a live tail.
    
    The feed is a notification lane, not an audit log:
    
    - Deduplicate by event id — polls repeat what the live stream delivered, and
      an event can appear up to ~30 seconds after it happens.
    - Deduplicate inbox items by `addressing_id`, never by event id: one event
      addresses you once per reason and each reason is its own item.
    - Events are thin pointers. Refetch the recording (`basecamp show
      <recording_id>`) before acting on it.
    - An agent that acts on what it hears should pass `--exclude-performers self`,
      which drops its own performances without hiding other agents' activity.
    
    The two lanes have different filter sets, and they are not interchangeable:
    
    - `events poll`: `--types`, `--buckets`, `--creators`, `--performers`,
      `--exclude-performers`, `--actor-types`.
    - `inbox`: `--reasons`, `--types`, `--buckets`. The other four are not flags
      here.
    
    Each takes a comma-separated list. These are the only narrowing available:
    both commands accept the global `--in <project>` flag but ignore it, because
    these are account endpoints. Narrow to a project with `--buckets`.
    
    A position is bound to the filter set it was minted for, so a resume has to
    carry the same filters. Changing them exits `1` naming both filter digests,
    and the fix is to re-enter with `--since`. A position that is no longer
    servable exits `2`, and the hint carries the whole re-entry command, filters
    included.
    
    `basecamp inbox` is served to agent principals only for now; other principals
    get exit `4`. `basecamp events ticket` redacts the ticket and its URL unless
    `--show-secret` is passed — both are bearers, so never log either, and mint a
    fresh one per connection attempt.
    
    ### Recordings (Cross-project)
    
    Use `basecamp recordings <type>` for cross-project type browsing. **For assigned todos, prefer `basecamp reports assigned`** — recordings do not include assignee data and cannot be filtered by person.
    
    ```bash
    basecamp recordings todos --json                  # All todos across projects
    basecamp recordings todos --all --json            # All todos (paginate through all)
    basecamp recordings messages --in <project>       # Messages in project
    basecamp recordings documents --status archived   # Archived docs
    basecamp recordings cards --sort created_at --direction asc
    basecamp recordings cards --status archived --all --json  # Include archived cards
    ```
    
    **Types:** `todos`, `messages`, `documents`, `comments`, `cards`, `uploads`
    
    **Status filtering:** By default, only `active` recordings are returned. Use `--status archived` or `--status trashed` to query other statuses. You may need separate queries to get complete data (e.g., active + archived).
    
    **Status management:**
    ```bash
    basecamp recordings trash <id> --in <project>     # Move to trash
    basecamp recordings archive <id> --in <project>   # Archive
    basecamp recordings restore <id> --in <project>   # Restore to active
    basecamp recordings visibility <id> --visible --in <project>  # Show to clients
    basecamp recordings visibility <id> --hidden      # Hide from clients
    ```
    
    ### Templates
    
    ```bash
    basecamp templates list --json                    # List project templates
    basecamp templates show <id> --json               # Project template details
    basecamp templates create "Template Name"         # Create empty project template
    basecamp templates update <id> --name "New Name"
    basecamp templates delete <id>                    # Trash project template
    basecamp templates construct <id> --name "New Project"  # Create project (async)
    basecamp templates construct <id> --name "New Project" --start-date 2026-09-01  # Anchor template dates to that week
    basecamp templates construction <template_id> <construction_id>  # Check project status
    
    basecamp templates library --json                 # List active to-do list templates
    basecamp templates copy <template_id> --in <project>  # Start copying into To-dos
    basecamp templates copy-status <copy_id>          # Check copy status
    ```
    
    **Asynchronous results:** `construct` returns a construction ID; poll `construction`
    until `status="completed"` to get the project. A template's dates are relative to its
    first week, and template weeks start on Sunday: `--start-date` (YYYY-MM-DD or natural,
    e.g. `next monday`) anchors them to the Sunday on or before that date; without it they
    anchor to the week of construction. `copy` returns a copy ID; poll
    `copy-status` through `pending` and `processing` until it is `completed` or `failed`.
    
    A copy can report the people who need access to the destination project. Show those
    people to the user and rerun with `--confirm-adding-p

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related