Harbor is a containerized LLM toolkit — a large Docker Compose project with a CLI and a Tauri app for managing AI services. Not to be confused with Harbor container registry which is a completely different unrelated project. This repository, Harbor, is the LLM toolkit.
harbor.sh— main CLI (too large to read in full; search for specific functions)services/— all service directories and compose files (e.g.,services/ollama/,services/compose.ollama.yml)compose.yml— base compose file, always includedapp/— Tauri GUI appdocs/— service and user documentationroutines/— CLI internals rewritten in Deno.scripts/— dev scripts in Deno/Bash, run viaharbor dev <script>tests/— container-based test runner (suites, rows, orchestrator); seetests/README.md.scripts/lint/— bash-compat lint rules (HARBORxxx), fixtures, and 4-pass orchestratorprofiles/default.env— default config distributed to usersskills/harbor/SKILL.md— agent-facing CLI skill (shipped via npm for Claude Code discovery)
harbor ps # list running containers
harbor ls # list all available services
harbor up <service> # start service(s)
harbor down # stop and remove containers
harbor logs <service> # ⚠️ TAILS BY DEFAULT (HANGS AGENT). Use docker logs <container> instead
harbor build <service>
harbor shell <service> # interactive shell in container
harbor exec <service> <cmd>
harbor eject # output standalone Compose config for current selection
$(harbor cmd <service>) # raw docker compose command for a serviceharbor config get <KEY>
harbor config set <KEY> <VALUE>
harbor config update # propagate profiles/default.env → .env
harbor config search <query> # search config keys and valuesNever edit .env directly — always use harbor config get/set.
harbor env <service> # list override vars for a service
harbor env <service> <key> # get a specific var
harbor env <service> <key> <value> # set a specific varharbor dev scaffold <service_name> # scaffold a new service
harbor dev docs # regenerate docs
harbor dev seed # seed test data
harbor dev add-logos [--dry-run] # resolve and write service logos
harbor dev test [--suite ...] [--distros ...] [--json] # container test matrix
harbor dev lint [--shellcheck|--rules|--compose|--boost] [--json] # 4-pass source lint
harbor dev lint-self-test # validate lint rules against fixturesDev scripts live in .scripts/ and must be run via harbor dev, not deno run directly.
harbor routine <name> # run internal Deno routines (routines/)harbor skills # list available agent skills
harbor skills get <name> # show a skill's content
harbor skills get <name> --full # show skill + references and templates
harbor skills path [name] # print skill directory pathUse the new-service skill: .agents/skills/new-service/SKILL.md.
After editing profiles/default.env, run harbor config update to apply changes to the current .env. The two files are not automatically synced.
services/compose.x.<service>.<integration>.yml files are applied when multiple services run together. When a satellite service can use a backend (e.g., Ollama):
- Add
depends_onfor the backend - Mount config templates needed for the integration
- Set environment variables
- Override entrypoint if config rendering is needed at startup
Example: services/compose.x.photoprism.ollama.yml
- Default model:
HARBOR_<SERVICE>_MODELinprofiles/default.env - Config templates use
${HARBOR_*}vars rendered at container startup - Run
harbor config updateafter changingprofiles/default.env
After any change to service shape (volumes, config, integrations), update the corresponding doc in docs/ immediately. Cover all new env vars, startup behaviors, and integration steps.
Logos are static URL strings in app/src/serviceMetadata.ts, resolved once via:
harbor dev add-logos # resolve and write
harbor dev add-logos --dry-run # preview onlyResolution order: GitHub homepage favicon → dashboardicons.com → GitHub owner avatar.
- Comments only for non-obvious logic — never restate what the code does
- No emojis in UI or copy — use Lucide icons instead
# RPM (Fedora — ayatana env vars required)
TAURI_LINUX_AYATANA_APPINDICATOR=1 PKG_CONFIG_PATH="$HOME/.local/lib/pkgconfig" npx tauri build --bundles rpmFedora ships libayatana-appindicator-gtk3 instead of libappindicator-gtk3. A local .pc file at ~/.local/lib/pkgconfig/ayatana-appindicator3-0.1.pc provides the missing pkg-config entry. Without these env vars the bundler panics.
When updating the ## News / changelog section in the README.md, always use a bulleted list format: - **vx.x.x** - one sentence. Do not use a table.
Release notes are user-facing changelog, not commit messages. Match the style of prior releases (fetch with gh release view vX.Y.Z --json body).
- One sentence per bullet about user-observable change. No
—cause clauses, no implementation rationale. - Skip changes with no user-visible effect (internal refactors, lint fixes, polish on features introduced in the same release).
- Lead with the symptom or capability, not the mechanism. "Workspace bind mounts now stay owned by your host user" not "
workspace-initsidecar pattern rolled out." - Don't enumerate full lists of affected services in-bullet. Say "rolled out to 17 services."
Never set llamacpp.model (HARBOR_LLAMACPP_MODEL) config. The router discovers models from the HF cache automatically. Setting it overrides that behavior.
This project uses facts for specification and documentation. All work flows through the fact sheet — it is the source of truth.
Every change starts with a fact. Facts are the spec — they define what "done" means. Code that isn't described by a fact is unverifiable and will be treated as incorrect. The skill facts skills show facts has the full format spec and command reference.
facts list— read the current spec to orient. Fact sheets can be large — use filters to focus:--section "cli/init",--tags "draft",--file api.facts,--manual. Read only the section relevant to your task, not the entire sheet.facts add— write facts describing what should be true when done. Each fact is a testable claim. You are not ready to write code until this step is complete.- Implement the code to make those facts true
facts check --tags "<tag>"orfacts get <id>— verify your changes. Never run barefacts checkunless asked.facts edit <id> --add-tag implemented— mark verified facts done
Step 4 only works if step 2 happened. If you skipped step 2, go back now — you cannot verify work that has no fact.
Manual facts (? in check output): these have no command, so you verify them by reading the relevant code. For each ? fact: read what it claims, check the code, report PASS or FAIL with a one-line reason. Reporting "N manual" without verifying each one is not acceptable.
Lifecycle: @draft → @spec → @implemented
Domain: the ## domain section in .facts defines the project's entities and relations — read it first to learn the vocabulary.
Skills (invoke via facts skills show <name>):
facts-refine— sharpen@draftfacts into@specwith the userfacts-discover— scan the codebase and sync facts to reality (only when explicitly asked)facts-implement— implement@specfacts in code, verify, tag@implemented