# Hourfield CLI

## App Store access

The CLI shares the running Mac app's verified App Store access. `app status`
includes `access` and `canAddWork`. Creating or editing work without an active
subscription, trial, or lifetime purchase returns `purchase_required` before a
save or mutation journal is written. Records, reports, full exports,
preferences, deletion, and finishing an existing timer remain available. Open
Hourfield Settings → Hourfield access to choose a plan, restore purchases, or
manage an Apple subscription. The CLI never makes purchases for you. Trial
eligibility and purchase access are not imported from files or copied through
iCloud data sync.

Availability: the signed universal Mac CLI is public at
<https://tieoff.app/apps/hourfield/agents#cli-download>. It requires a
Hourfield Mac 1.1.0 build 3 or later with Agent access. The Mac app must be
running; app beta access remains by invitation. The universal download supports
Apple silicon and Intel Macs on macOS 14 or later and contains no app source.
The app's public App Store release is pending Apple's review.

The native `hourfield` executable lets your local agents control Hourfield on your Mac. Open Hourfield and turn on **Settings → Agent access**. Access is off by default. The app processes commands through its current state and normal atomic save flow. The CLI never reads or writes `hours.json` directly.

Website: <https://tieoff.app/apps/hourfield>. Installation and agent guide: <https://tieoff.app/apps/hourfield/agents#cli-download>.

## Install and check

```sh
curl -fLO https://tieoff.app/apps/hourfield/downloads/hourfield-cli-1.0.0-mac-universal.zip
curl -fLO https://tieoff.app/apps/hourfield/downloads/hourfield-cli-1.0.0-mac-universal.zip.sha256
shasum -a 256 -c hourfield-cli-1.0.0-mac-universal.zip.sha256
unzip hourfield-cli-1.0.0-mac-universal.zip
./Hourfield-CLI-1.0.0/install.sh
export PATH="$HOME/.local/bin:$PATH"
hourfield --version
hourfield --json doctor
hourfield --json capabilities
```

The ZIP contains a signed universal executable for Apple silicon and Intel Macs running macOS 14 or later. The installer verifies its binary checksum and Developer ID signature, installs it in `~/.local/bin/hourfield`, and keeps a hash receipt. It refuses to overwrite an executable or symlink it cannot recognize. An optional first argument selects another installation prefix: `./Hourfield-CLI-1.0.0/install.sh /my/prefix`.

`doctor` is a local status read. Its `data.ready` is true only when the app heartbeat is fresh, access is enabled, and the app is unlocked. `hourfield docs` prints the website URL without launching a browser. `HOURFIELD_WEBSITE_URL` or `hourfield docs --website-url HTTPS_URL` overrides that URL.

The default app data directory is:

```text
~/Library/Containers/com.bennyg.hoursdesktop/Data/Library/Application Support/Hours Desktop
```

Use `HOURFIELD_DATA_DIR` or `--data-dir PATH` for an explicitly chosen alternate store. The Mac app must be running with that same store. All development fixtures must be isolated from personal records.

## Stable output and errors

Pass `--json` anywhere. Each normal result is one JSON line:

```json
{"schema":1,"ok":true,"data":{"items":[],"total":0,"limit":100,"offset":0},"error":null,"requestID":"COMMAND-UUID"}
```

Failures use `ok:false` and `error:{code,message,retryable}`. Exit status is 0 for success, 2 for syntax/unknown-command errors, and 1 for other failures. `--version` prints a plain version string unless `--json` is present. Monetary totals are decimal strings so agents can retain exact values. Timestamps in results use ISO 8601; record revisions hash the exact internal value.

Important errors include `agent_disabled`, `app_unavailable`, `app_locked`, `editor_busy`, `conflict`, `preview_conflict`, `id_conflict`, `invalid_argument`, `save_failed`, `output_exists`, and `outcome_unknown`. An open GUI editor or import preview blocks record mutations so agents cannot overwrite unsaved human work. Finish it in the app or explicitly run `hourfield ui dismiss`.

A request waits up to 10 seconds by default. Use `--timeout SECONDS` (a whole number from 1 to 120) when a normal Touch ID or system permission prompt needs more time, for example `hourfield --timeout 120 app unlock`. A timeout does **not** establish whether the app saved it. Inspect state, or replay the identical command and arguments using the original `--request-id UUID`. Never reuse an ID for a different payload. Receipts are bound to a SHA256 fingerprint of command, arguments, protocol, and dry-run state. Creating/copying jobs, entries, saved views, and timers uses the request UUID for deterministic identity.

## Dry runs, patches, and concurrent edits

All engine mutations accept `--dry-run`: validation and results describe the proposal, but the app's archive is unchanged. App actions support dry runs where meaningful; status and preview commands already read only. Dry-run exports do not write the destination file.

Jobs, entries, preferences, and saved views accept a partial `--patch JSON` or `--patch-file PATH`. JSON keys are the camelCase model fields in the tables below. Values retain JSON types: numbers, booleans, strings, arrays, or permitted nulls. Unspecified fields retain their exact values. Array patches replace that entire array. Unknown fields/options and incorrect types produce errors.

```sh
hourfield --json jobs show JOB_UUID
hourfield --json jobs update JOB_UUID \
  --patch '{"rate":42.5,"dailyOvertime":9,"notes":"New agreement"}' \
  --if-match REVISION_FROM_SHOW --dry-run
```

`show`/`get` and mutation results include a `revision`. Optional `--if-match REVISION` rejects a stale update. Partial updates are applied to the app's latest archive, so changes to other fields survive. Use `--if-match` when your decision depends on the exact version you read. This is supported for job update/archive/copy, entry update/copy/delete, preferences set, saved-view update/delete, and timer break/resume/clock-out.

## Jobs

```sh
hourfield jobs list [--archived true|false] [--query TEXT] [--limit 100 --offset 0]
hourfield jobs show JOB_UUID
hourfield jobs create --name Consulting --rate 85 --currency USD
hourfield jobs update JOB_UUID --patch-file job-changes.json
hourfield jobs archive JOB_UUID
hourfield jobs archive JOB_UUID --archived false
hourfield jobs copy JOB_UUID --name 'Consulting copy'
```

Job selectors accept a UUID or an exact unique name. Prefer UUIDs. A running job must be clocked out before it can be archived. New rates apply to new entries; existing entries retain their saved rate. Other job rules recalculate that job's history, matching the app.

| Patch field | Type / values | Corresponding flag |
| --- | --- | --- |
| `name`, `notes` | string | `--name`, `--notes` |
| `color` | blue, purple, green, orange, pink, teal | `--color` |
| `rate`, `withholding`, `mileageRate` | number; withholding is percent | `--rate`, `--withholding`, `--mileage-rate` |
| `currency` | three-letter code | `--currency` |
| `dailyOvertime`, `dailyDoubleTime`, `weeklyOvertime` | hours; 0 disables threshold | `--daily-overtime`, `--daily-double-time`, `--weekly-overtime` |
| `overtimeMultiplier`, `doubleTimeMultiplier` | number | `--overtime-multiplier`, `--double-time-multiplier` |
| `rounding` | None, Nearest, Up, Down | `--rounding` |
| `roundingMinutes` | integer 1–120 | `--rounding-minutes` |
| `autoBreakAfterHours`, `autoBreakMinutes`, `targetHours` | number | `--auto-break-after-hours`, `--auto-break-minutes`, `--target-hours` |
| `schedule` | Weekly, Every two weeks, Twice a month, Monthly | `--schedule` |
| `payAnchor` | ISO timestamp | `--pay-anchor` |
| `archived` | boolean | `--archived` |
| `latitude`, `longitude`, `locationRadius` | numbers; radius meters | `--latitude`, `--longitude`, `--location-radius` |
| `locationMode` | Off, Remind, Automatic | `--location-mode` |

## Timers

```sh
hourfield timer status [--job JOB_UUID]
hourfield timer clock-in JOB_UUID
hourfield timer break [ENTRY_UUID] --paid false
hourfield timer break [ENTRY_UUID] --paid true
hourfield timer break [ENTRY_UUID] --kind Pause --paid false
hourfield timer resume [ENTRY_UUID]
hourfield timer clock-out [ENTRY_UUID]
```

When exactly one timer runs, its entry UUID is optional. Starting a new job closes the previous timer and any open break in single-timer mode. Starting an already active job leaves it alone. Starting the same type of break, resuming an already resumed timer, or clocking out an already ended explicitly selected entry leaves the current state unchanged. Resume before changing from paid to unpaid break or the reverse.

## Entries

```sh
hourfield entries list --range week --job JOB_UUID --running false --limit 100
hourfield entries show ENTRY_UUID
hourfield entries create --job JOB_UUID \
  --start 2026-10-22T09:00:00-04:00 --end 2026-10-22T17:00:00-04:00 \
  --tips 12 --tags 'client,billable' --comment 'On-site work'
hourfield entries update ENTRY_UUID --patch-file entry-changes.json
hourfield entries copy ENTRY_UUID
hourfield entries delete ENTRY_UUID --dry-run
```

Entry create requires a job and explicit start field or patch. If omitted, end defaults to the app's current time; `--running true` starts a running manual entry. Job changes accept `--job JOB_UUID` or patch `jobID`. Editing an entry's job retains its rate unless you explicitly change the rate. Copying a running entry creates a completed copy and closes its copied open break.

| Patch field | Type / meaning | Flag |
| --- | --- | --- |
| `jobID` | job UUID string | `--job-id` or `--job` |
| `start`, `end` | ISO timestamp; end may be null for running | `--start`, `--end` |
| `pauses` | complete array of break objects | `--pauses JSON_ARRAY` |
| `manualBreakMinutes`, `timeAdjustmentMinutes` | number; adjustment may be negative | `--break-minutes`, `--time-adjustment-minutes` |
| `rate`, `tips`, `miles`, `adjustment` | numbers; adjustment may be negative | corresponding kebab-case flag |
| `adjustmentTaxable` | boolean | `--adjustment-taxable` |
| `comment`, `source` | string | corresponding flag |
| `tags` | array of strings | `--tags JSON_ARRAY` or comma-separated text |

A break object contains `id` (UUID), `start`, `end` (timestamp or null), `paid` (boolean), and `kind` (string). Breaks must be ordered, non-overlapping, and inside the entry. At most one break may remain open. Example:

```json
{"pauses":[{"id":"C327E07E-6922-44B3-9B7E-17808E8DA975","start":"2026-10-22T12:00:00-04:00","end":"2026-10-22T12:30:00-04:00","paid":false,"kind":"Break"}]}
```

## Preferences, tags, and saved views

```sh
hourfield preferences get
hourfield preferences set --patch '{"decimalHours":true,"weekStartsOn":2}'
hourfield tags list
hourfield tags rename 'old name' 'new name'
hourfield tags merge 'old name' 'existing name'
hourfield tags remove 'unused label'
hourfield filters list
hourfield filters create --name 'Client this month' --range 'This month' --query client
hourfield filters update FILTER_UUID --patch '{"jobID":null,"tag":"billable"}'
hourfield filters delete FILTER_UUID
```

Preferences expose **every stored app preference**: `multipleTimers`, `decimalHours`, `weekStartsOn` (1 Sunday–7 Saturday), `timeZone` (IANA identifier), `lockOnLaunch`, `remindersEnabled`, `reminderHour`, `reminderMinute`, and `reminderWeekdays`. Flags use kebab-case. Turning off multiple timers while two run fails validation. Notification and location permissions are requested separately through the normal macOS dialogs.

Saved-view patch fields: `name`, `query`, `tag`, `jobID` (UUID or null), `range`, `from`, `through`. Range values are the app's labels: `Today`, `This week`, `This month`, `This year`, `Pay period`, `All time`, `Custom`. Dates are ISO timestamps. Renaming/merging/removing tags updates matching saved views and preserves the entries.

## Reports and selection

```sh
hourfield reports summary --range month
hourfield reports daily --from 2026-10-01 --through 2026-10-31
hourfield reports payperiod --job JOB_UUID --at 2026-10-22T12:00:00-04:00
hourfield reports print --range month --timeout 120
```

Common selection flags for entry lists, reports, and CSV/XLSX exports:

| Flag | Meaning |
| --- | --- |
| `--job UUID` | specific job |
| `--query TEXT` | job name, entry comment, or tags |
| `--tag NAME` | exact tag |
| `--range all|today|week|month|year|pay-period` | defaults to all; pay period requires a job |
| `--from DATE` | inclusive start |
| `--through DATE` | date-only includes that full day; timestamp is exclusive |

Date-only values use the app's reporting timezone. Entry lists select overlapping entries. Reports select calendar-day payroll slices and calculate overtime using the **complete history before filtering**. Use date-only bounds for daily payroll reports. Money stays grouped by currency. Net is the app's estimate: gross minus withholding plus mileage reimbursement. `reports daily` also returns per-day, per-job totals. `reports payperiod` returns the selected job's configured period. `--at` selects the reporting reference time for historical periods and running timers.

List pagination defaults to `--limit 100 --offset 0`, with a maximum limit of 1000. An offset beyond the end returns an empty page and the full matching total.

## Export, import, and local backups

```sh
hourfield export json --out "$HOME/Desktop/Hourfield Backup.json"
hourfield export csv --range month --out "$HOME/Desktop/Hourfield Month.csv"
hourfield export xlsx --range month --out "$HOME/Desktop/Hourfield Report.xlsx"
hourfield export txt --range month --out "$HOME/Desktop/Hourfield Report.txt"
hourfield --json import preview '/path/Hourfield Backup.json'
hourfield --json import apply '/path/Hourfield Backup.json' --preview TOKEN_FROM_PREVIEW
hourfield backups list
hourfield backups preview BACKUP_FILENAME
hourfield backups restore BACKUP_FILENAME --preview TOKEN_FROM_PREVIEW
```

JSON export is a complete backup; it rejects filters. CSV exports completed entries using the app's CSV format. XLSX exports payroll rows. `txt` (or its alias `text`) exports a formatted UTF-8 report with daily job rows and separate totals for each currency, using the same complete-history payroll selection. Report exports and printing accept `--at ISO_DATE` to select a reference time. Export paths never overwrite existing files or symlinks. Without `--out`, the returned data contains base64 content for another local client to handle.

`hourfield reports print` accepts the same `--job`, `--range`, `--from`, `--through`, `--query`, `--tag`, and `--at` report selections and opens the normal Mac **Print / Save as PDF** panel. Choose the printer or PDF destination in that panel; the CLI does not silently print or save a PDF. Its dry run previews the action without opening the panel.

JSON import replaces the complete archive; CSV merges completed entries. A SHA256 preview token binds the exact input bytes **and** current archive. The CLI sends the file contents to the running app, which previews and validates them. Apply requires the token and unchanged content/state. The app saves a rolling backup before a replacement. A CLI input file is limited to 600,000 bytes so its base64 request fits the 1 MiB transport limit. Larger imports remain available through the app GUI.

`backups list` returns app-managed local backup filenames. Preview and restore accept only those IDs; they cannot open arbitrary paths inside the app. Restore uses the same preview token check.

## App and service controls

```sh
hourfield app status
hourfield app show
hourfield app settings
hourfield app help
hourfield app data-folder
hourfield app lock
hourfield app unlock
hourfield ui navigate overview
hourfield ui navigate entries
hourfield ui navigate calendar
hourfield ui navigate reports
hourfield ui navigate job --id JOB_UUID
hourfield ui navigate filter --id FILTER_UUID
hourfield ui edit-job JOB_UUID
hourfield ui edit-entry ENTRY_UUID
hourfield ui dismiss
hourfield cloud status
hourfield cloud enable
hourfield cloud disable
hourfield cloud sync
hourfield notifications status
hourfield notifications request
hourfield location status
hourfield location start
hourfield location stop
```

Unlock uses normal LocalAuthentication. The CLI cannot bypass authentication or silently grant macOS permissions. When locked, only app status, show, and normal unlock can be requested; private record operations remain blocked. iCloud, location monitoring, and notification behavior match the running app. Each tester uses their own private iCloud records.

`hourfield app data-folder` opens the running app's current data directory in Finder. Its dry run reports the proposed action without opening Finder.

## Generic command access

Every discovered command is also available through the generic route:

```sh
hourfield --json command jobs.update --args-file update-arguments.json --dry-run
hourfield --json command cloud.status --args-file empty-object.json
```

An arguments file is a JSON object. String values pass through directly; numbers, booleans, arrays, and objects become JSON text for the command engine. For a patch:

```json
{"id":"JOB-UUID","patch":{"rate":85,"dailyOvertime":8}}
```

Transport import arguments use `{format:"json"|"csv",content:"BASE64",preview:"TOKEN"}`. Preview omits the token. Export returns `{filename,mimeType,bytes,base64,sha256}`. Raw named commands still use all app gates, validation, journaling, dry runs, and argument checks.

## Local protocol

Protocol version 1 uses JSON envelopes with ISO 8601 dates. Requests are atomically published at `AgentAPI/Requests/<UUID>.json`, with replies at `AgentAPI/Responses/<UUID>.json`. Directories have mode 700 and files mode 600. The CLI refuses symlink transport paths, stale heartbeats over 3 seconds, disabled access, mismatched reply identities, and conflicting receipt fingerprints. Requests are limited to 1 MiB; replies read by the client are limited to 32 MB.

The app validates filename/UUID, protocol version, request age (60 seconds), user access, lock/readability state, current archive, and save outcome. It journals record changes before committing and retains replay receipts. The transport has no sockets, web server, cloud credentials, or network access. It grants control to processes already running as the same Mac user while Agent access is enabled; turn it off to stop future commands. Queued exports and receipts can contain private records, so keep the app's data directory private.

## Rename compatibility

Hourfield keeps the existing bundle IDs, private iCloud container, preferences, and data directory shown above. No records are moved. `HOURFIELD_DATA_DIR` and `HOURFIELD_WEBSITE_URL` take precedence; the legacy `HOURS_DESKTOP_DATA_DIR` and `HOURS_WEBSITE_URL` variables still work. The installer also updates a recognized existing `hours` command to the same renamed executable for script compatibility.
