hubstaff is a command-line interface for the Hubstaff Public API. Use it to work with organizations, projects, members, invites, tasks, and activity data directly from your terminal.
Homebrew (macOS / Linux)
brew install netsoftholdings/tap/hubstaffShell installer (macOS / Linux)
curl --proto '=https' --tlsv1.2 -LsSf https://github.com/NetsoftHoldings/hubstaff-cli/releases/latest/download/hubstaff-installer.sh | shWindows
powershell -ExecutionPolicy Bypass -c "irm https://github.com/NetsoftHoldings/hubstaff-cli/releases/latest/download/hubstaff-installer.ps1 | iex"# One-time auth
hubstaff config set-pat YOUR_PERSONAL_ACCESS_TOKEN
hubstaff config set organization 12345 # optional default org
# Discover what you can do
hubstaff list # every command, grouped by resource
hubstaff projects --help # group help with summaries
hubstaff projects list --help # full flag reference for one operation
# Read operations
hubstaff users me
hubstaff projects list
hubstaff projects list --page_limit 10
hubstaff projects get 123
hubstaff -p projects list # pretty, colorized JSON
# Write operations (side-effecting)
hubstaff projects create --body-json '{"name":"New","status":"active"}'
hubstaff projects update 123 --body-file patch.json
hubstaff teams update_members 42 --body-json '{"members":[{"user_id":7,"role":"member"}]}'
# Per-invocation org override
hubstaff -o 999 projects list
# Health check (exit 0 = all OK, exit 1 = any FAIL)
hubstaff check
# Non-interactive / CI: scope a config dir per run, then set-pat
export XDG_CONFIG_HOME=/tmp/hubstaff-$CI_RUN_ID
hubstaff config set-pat "$HUBSTAFF_PAT"
hubstaff -j projects list # minified JSON, pipeable to jqExit codes: 0 success · 1 API error · 2 auth error · 3 config / usage error · 4 network error.
Authenticate once with a personal access token from the Hubstaff developer portal; the CLI exchanges it for access + refresh tokens and auto-refreshes from then on.
hubstaff config set-pat YOUR_PERSONAL_ACCESS_TOKENset-pat treats the value as a refresh token and POSTs grant_type=refresh_token to <auth_url>/access_tokens. The response's access_token, refresh_token, and expires_in are saved to the [auth] section of the config file. After this, every API call auto-refreshes the access token when it is within 120 seconds of expiry, and transparently refreshes once on a 401 response.
CI pipelines should scope a per-run config dir so credentials don't leak between jobs:
export XDG_CONFIG_HOME=/tmp/hubstaff-$CI_RUN_ID
hubstaff config set-pat "$HUBSTAFF_PAT"
hubstaff users mehubstaff config set token RAW_ACCESS_TOKEN— stores a raw access token directly and clears anyrefresh_token/expires_at. Will not auto-refresh. Useset-patunless you have a specific reason to bypass the exchange.hubstaff config unset token— clears the whole[auth]section.hubstaff config reset— resets every key including tokens.
Global flags work with any subcommand and can appear before or after it.
| Flag | Short | Type | Effect | Notes |
|---|---|---|---|---|
--json |
-j |
bool | Emit minified single-line JSON | Overrides config format. Conflicts with -p. |
--pretty |
-p |
bool | Emit pretty-printed, colorized JSON | Overrides config format. Conflicts with -j. |
--organization N |
-o N |
u64 | Override the default organization for this invocation | Takes precedence over config.organization. |
--help |
-h |
bool | Print context-appropriate help | See Help system. |
--version |
— | — | Print hubstaff <version> |
From CARGO_PKG_VERSION at build time. |
If neither -j nor -p is passed, the config format value applies (default json).
Top-level commands are fixed: config, list, check, plus a dynamic subcommand that dispatches anything else to the schema-driven API layer.
Manage configuration. All subcommands read and write $CONFIG_DIR/hubstaff/config.toml — see Configuration for the resolution of $CONFIG_DIR.
hubstaff config set KEY VALUE # set a single key
hubstaff config unset KEY # clear KEY / restore its default
hubstaff config reset # wipe everything, including [auth]
hubstaff config set-pat TOKEN # exchange a PAT for access + refresh tokens
hubstaff config show # print current config (tokens masked as ****)Valid keys for set and unset:
| Key | Type | Default | Notes |
|---|---|---|---|
organization |
u64 | (unset) | Must parse as u64. Used as default org id when a required organization_id is omitted. |
api_url |
URL | https://api.hubstaff.com/v2 |
Base URL for API calls. |
auth_url |
URL | https://account.hubstaff.com |
OAuth server for set-pat and refresh. |
schema_url |
URL | (derived from api_url + /docs) |
Explicit override for the schema source. |
token |
string | (unset) | Raw access token; clears refresh_token and expires_at. |
format |
json | pretty |
json |
Default output format when neither -j nor -p is passed. |
set token and set-pat both mask the echoed value as ****. show never prints raw token material and omits auth_url when it equals the default.
Prints every API command the local schema knows about, grouped by resource, summaries aligned. Stable as long as the cached schema does not change.
Runs nine diagnostics and prints a table with one of four status markers per row: OK, WARN, FAIL, SKIP. Exit code is 1 if any row is FAIL, else 0. Warnings do not fail the check. See Diagnostics reference for per-check behavior.
Any positional token that is not config, list, or check is matched against the cached schema. General shape: hubstaff <words...> [path_id...] [query-flags...] [body-flags].
<words...>— command words derived from the URL path (see below).[path_id...]— one positional per visible path parameter, in URL-template order, excluding the implicitorganization_id.- Query flags —
--<param> VALUE,--<param>=VALUE, or the generic--query NAME=VALUE. - Body flags —
--body-json '<JSON>'or--body-file <PATH>(mutually exclusive), or an operation-specific body alias like--<param> <JSON>when the schema declares a body parameter with that name.
Commands are derived from the OpenAPI path and HTTP method at index-build time. A leading /organizations/{organization_id}/NEXT/... is rewritten to /NEXT/... unless NEXT starts with update_ (so /organizations/{organization_id}/update_members is preserved verbatim — update_* paths take an explicit organization path argument). Static segments become command words; {xxx} segments become positional arguments. A terminal action (list / create / get / update / delete) is appended based on the HTTP method and whether the path ends in a static segment or a {param}; non-standard action paths (e.g. .../update_members) use the literal last segment as the action with no synthesized suffix.
| Path template | Method | Command |
|---|---|---|
/organizations/{organization_id}/users/me |
GET |
hubstaff users me |
/organizations/{organization_id}/projects |
GET |
hubstaff projects list |
/organizations/{organization_id}/projects |
POST |
hubstaff projects create |
/organizations/{organization_id}/projects/{project_id} |
GET |
hubstaff projects get <project_id> |
/organizations/{organization_id}/projects/{project_id} |
PATCH |
hubstaff projects update <project_id> |
/organizations/{organization_id}/projects/{project_id} |
DELETE |
hubstaff projects delete <project_id> |
/organizations/{organization_id}/update_members |
PUT |
hubstaff update_members <organization_id> |
/teams/{team_id}/update_members |
PUT |
hubstaff teams update_members <team_id> |
-
Path parameters are always positional, in the order they appear in the path template.
-
organization_idis hidden: it is filled from-o/--organizationorconfig.organization. If no source provides it, the CLI errors withmissing required path parameter 'organization_id'. -
Providing a path parameter as a flag errors with
error: --<param> is a path parameter; provide it positionally: <usage>. -
Query parameters are flags:
hubstaff projects list --page_limit 10 hubstaff projects list --query page_limit=10 # generic form hubstaff activities list \ --time_slot[start] 2026-04-01T00:00:00Z \ --time_slot[stop] 2026-04-08T00:00:00Z -
Duplicate query names error with
query parameter '<name>' was provided multiple times.
Three ways to supply a JSON body (the first two are mutually exclusive):
hubstaff projects create --body-json '{"name":"New","status":"active"}' # inline JSON
hubstaff projects update 123 --body-file ./patch.json # from file
hubstaff webhooks create --webhook '{"url":"https://example.com/hook"}' # body alias (if schema declares one)The argument parser treats a token starting with -- as the next flag. To pass a value that begins with --, attach it with =:
hubstaff foo bar --body-json='--literal'-h / --help anywhere in the arguments triggers context-appropriate help. Four flavors:
- Global help — bare
hubstaffor an unknown first word: usage shape, examples, andhubstaff listfor discovery. Unknown-prefix matches get up to 8Suggestions:. - Group help —
hubstaff <prefix> --help(e.g.projects,activities daily): lists subcommands with summaries aligned. - Operation help —
hubstaff <full-command> --help: method, path, summary, description, tags, and every query option with its type and description. If descendant subcommands exist, they are appended. - Shape-mismatch help — input resolves to a command but with the wrong number of positional path arguments:
'<cmd>' expects N path argument(s): <param names>.
$CONFIG_DIR/hubstaff/config.toml, where $CONFIG_DIR is resolved as:
$XDG_CONFIG_HOMEif set.- Else the OS default (macOS:
~/Library/Application Support; Linux:~/.config; Windows:%APPDATA%viadirs::config_dir()). - Else
$HOME/.config.
On Unix, $CONFIG_DIR/hubstaff is created with mode 700. hubstaff check raises a WARN if the mode drifts; fix with chmod 700 <dir>.
api_url = "https://api.hubstaff.com/v2"
auth_url = "https://account.hubstaff.com" # omitted from disk when equal to default
organization = 12345 # optional
schema_url = "https://api.hubstaff.com/v2/docs" # optional; derived from api_url when absent
format = "json" # omitted from disk when equal to default
[auth] # entire section omitted when all fields are None
access_token = "..."
refresh_token = "..."
expires_at = 1775347200 # unix timestampWrites are atomic: the CLI writes to a sibling temp file and renames, so a crash mid-write cannot corrupt the file. Keys and their defaults are documented in hubstaff config.
| Variable | Effect |
|---|---|
XDG_CONFIG_HOME |
Overrides the config base directory. |
| Source of format decision | Winner |
|---|---|
-j / --json on the command line |
minified single-line JSON |
-p / --pretty on the command line |
pretty-printed colorized JSON (via colored_json) |
config.format = "json" |
minified |
config.format = "pretty" |
pretty |
| default | minified |
Pretty output is colorized on TTYs and degrades gracefully on pipes.
The CLI does not ship a static list of endpoints. It fetches the Hubstaff OpenAPI schema and builds an in-memory command index on first use.
All under $CONFIG_DIR/hubstaff/schema/v2/:
| File | Content |
|---|---|
docs.json |
Raw OpenAPI schema JSON. |
meta.toml |
Cache metadata: fetched_at, etag, schema_hash, source_url. |
command_index.json |
Pre-built command index (version, schema_hash, entries). |
On every invocation that needs the schema:
- Conditional
GETagainstconfig.effective_schema_url()with a 40 s timeout, sendingIf-None-Match: <cached etag>when the cachedsource_urlmatches the currentschema_url. 200 OK— parse and replace the cache; write freshmeta.tomlwith new etag,fetched_at = now,source_url = <current schema_url>.304 Not Modified— keep the cached schema; updatefetched_atonly.- Fetch failed — fall back to the cached schema only if the cached
source_urlmatches the currentschema_url. Otherwise error withschema fetch failed: <cause>.
The source_url match is the cross-environment safety check: a docs.json fetched against staging cannot accidentally serve production commands after you config set api_url back to production.
command_index.json is keyed on INDEX_VERSION (currently 1) and the schema hash. When either changes, the index is rebuilt on the next run. Manual invalidation is almost never necessary.
There is no hubstaff schema refresh command. To force a re-fetch, delete the cache directory:
# macOS (default config dir)
rm -rf "$HOME/Library/Application Support/hubstaff/schema/v2"
# Linux / XDG
rm -rf "${XDG_CONFIG_HOME:-$HOME/.config}/hubstaff/schema/v2"The next command that needs the schema will refetch and rebuild the index.
hubstaff check runs the following checks in order. Status markers: OK, WARN, FAIL, SKIP. Exit code is 1 if any check is FAIL, else 0.
| # | Check | OK when |
WARN when |
FAIL when |
SKIP when |
Remediation on failure |
|---|---|---|---|---|---|---|
| 1 | CLI version | always | — | — | — | — |
| 2 | Config file | file loads (or is absent → defaults used) | — | TOML parse error | — | Fix TOML or delete the file to reset. |
| 3 | Config dir perms (Unix only) | mode is 700 |
mode is anything else, or stat fails |
— | directory does not exist; non-Unix platform | chmod 700 <dir> |
| 4 | Credentials | stored access/refresh token present | — | none present | — | hubstaff config set-pat <TOKEN> |
| 5 | Token validity | token is fresh, or near-expiry with a successful refresh, or expired with a successful refresh | near expiry (within 300 s) and no refresh token | lacks expires_at; expired without refresh token; or refresh attempt failed |
no credentials | hubstaff config set-pat <TOKEN> |
| 6 | API reachability | GET <api_url>/users/me returns 2xx (RTT reported) |
— | transport error or auth error | no credentials | Fix connectivity / api_url, or re-auth. |
| 7 | Organization access | GET /organizations/<org_id> succeeds |
— | request fails | no credentials, API unreachable, or no default organization | Verify the id with hubstaff config show, or pass --organization. |
| 8 | Schema cache | cache loads and is ≤ 30 days old | cache loads but fetched_at is > 30 days old |
cache is missing or fails to load | — | Delete the cache dir to force refetch. |
Each row prints a detail string and, where applicable, a remediation hint and a notes: block. The Schema cache row always includes notes: operations, url, fetched_at, etag (if any), and the three cache file paths.
Token-validity internals: near-expiry threshold is 300 s; proactive refresh skew during normal API calls is 120 s; during check, a near-expiry or expired token with a refresh token is refreshed in-place and the result is recorded.
All errors are written to stderr prefixed with error: (followed by a space). The process exits with a variant-specific code:
| Error variant | Exit code | Display format | Typical triggers |
|---|---|---|---|
Api { status, message } |
1 |
[<status>] <message> |
API returned 4xx/5xx; 429 rate limit; invalid JSON response. |
Auth(msg) |
2 |
<msg> |
401 responses; missing credentials; failed PAT exchange. |
Config(msg) |
3 |
<msg> |
Unknown command; invalid flags; TOML parse/write; IO; unsupported formData/header params; invalid JSON body. |
Network(msg) |
4 |
<msg> |
Timeout (40 s on API / schema / auth); connection refused; DNS failure; 5xx from auth service. |
HTTP timeouts are 40 seconds for API requests, schema fetches, and auth/token refresh. See Troubleshooting for the errors whose cause/fix is non-obvious from the message text.
No stored tokens.
hubstaff config set-pat YOUR_PERSONAL_ACCESS_TOKENThe stored access token was 401, refresh was attempted, and the retried request was still 401 (refresh token invalid or revoked). Re-run hubstaff config set-pat YOUR_PERSONAL_ACCESS_TOKEN.
The set-pat POST to <auth_url>/access_tokens returned non-2xx. Most commonly a revoked/expired PAT, or a mismatched auth_url.
- Verify the PAT in the Hubstaff developer portal.
- Confirm
auth_urlwithhubstaff config show. If customized for staging, make sure it matches. - Re-run
hubstaff config set-pat <TOKEN>.
The refresh POST did not return a usable 2xx. Two flavors:
- "...Check your internet connection and try again." — transport failure (timeout, DNS, connection refused). Check connectivity and
auth_url. - "...The auth service is unavailable; retry shortly." — auth service returned 5xx. Your credentials are fine; wait and retry.
In either case, stored tokens are untouched; no need to re-run set-pat.
The schema GET failed and there is no usable cached schema whose source_url matches the current schema_url. This is also what you'll see when you expect the CLI to fall back to an existing schema/v2/docs.json from a different environment — the source_url mismatch is deliberate, to prevent prod runs against a staging schema.
-
Verify
api_url/schema_urlwithhubstaff config show. -
If you are intentionally offline, confirm that
source_urlinschema/v2/meta.tomlequals the currentschema_url. If it doesn't, the cache will not be used by design. -
Align
schema_urlwith the cachedsource_url, or restore defaults:hubstaff config unset api_url hubstaff config unset schema_url
The argument parser treats -- prefix as the next flag. Attach the value with =:
# wrong — '--literal' becomes the next flag
hubstaff foo bar --body-json --literal
# right
hubstaff foo bar --body-json='--literal'MIT.