{% extends "base.html" %} {% import "macros.html" as m %} {% block title %}Settings{% endblock %} {% macro status(done, yes='done', no='pending') %}{{ yes if done else no }}{% endmacro %} {% macro step_errors(step) %}{% set errs = form_errors.get(step, {}) %}{% if errs %}
{% endif %}{% endmacro %} {% macro how(href) %}How it works{% endmacro %} {#- "or describe it": the same change, built by your own agent (Make it yours). -#} {% macro describe(section) %}or describe it{% endmacro %} {#- The page is five tabs. Each lists the section ids it holds, so a link or a redirect to a section (/settings#bcc) opens the tab it is on. -#} {% set groups = [ ('general', 'General', ['make-it-yours', 'you', 'enrichment', 'appearance', 'support', 'about']), ('mail', 'Email & calendar', ['bcc', 'send', 'calendar']), ('crm', 'Your CRM', ['playbook', 'fields', 'messaging', 'task-types', 'stages', 'outcomes']), ('backup', 'Data', ['backup', 'export']), ('advanced', 'Advanced', ['schedule', 'access', 'mcp', 'doctor']), ] %} {% set lb = local_backup %} {% set attention = { 'general': not state.you, 'mail': inbox_alert or send_banner, 'backup': lb.level != 'ok', } %} {% macro group_start(key) %}{% for gid, label, sections in groups if gid == key %}
{% endfor %}{% endmacro %} {% block content %}

Settings

{% if setup_pending %}

A few steps are still open below. Skip for now

{% endif %} {# ================================================================= General #} {{ group_start('general') }}

Make it yours with your AI

{{ how('/help/adjust') }}

Describe a change (a field, a dashboard, a new look, a routine) and your own AI agent builds it in your data folder, one undoable commit each.

{% if handoff.how == 'copy' %}You get a prompt to paste; name your agent under AI below for a one-click button.{% else %}Requests go to {{ handoff.label }}; change that under AI below.{% endif %}

You

{{ status(state.you) }}
{% if state.you %}

{{ you.name }}

Sends mail from {{ you.addresses }}

{% else %}

Your name signs the drafts; mail from your addresses counts as outbound.

{% endif %} {{ step_errors('you') }}
{{ 'Change name or addresses' if state.you else 'Your name and addresses' }}

AI: Enrich and Ask the Hermit

{%- if enrich_status.available %}In use: {{ enrich_status.provider }} {%- else %}Unavailable{% endif %}{{ how('/help/settings#ai-enrich-and-ask-the-hermit') }}

Fills a record's empty fields and answers questions about your CRM, using an AI command-line tool you already have.

{% if not enrich_status.available %}

Unavailable: {{ enrich_status.reason }}

Under launchd or systemd (hermitcrm schedule install --serve) the PATH is bare: put the CLI in /usr/local/bin, /opt/homebrew/bin or ~/.local/bin, or give its full path as the command below.

{% endif %} {{ step_errors('enrichment') }}
Account
Model Strong: {{ model_label(tier_models.strong) }}
Command, timeout and model ids

Only for a CLI that names a model differently; an id set here is used on any account. {%- call m.hint('model-ids-help', 'Why a subscription asks for no model') %} A subscription is not entitled to every API model id, so on one Hermit CRM asks for no particular model where that matters.{% endcall %}

Make it yours hands requests to

The agent that builds what you describe under Make it yours, in {{ handoff.folder_shown }}. {%- call m.hint('agent-handoff-help', 'How each agent is started') %} Claude Code and Cursor open with one click; Codex and Gemini CLI get a command to paste in a terminal; with none, you copy the prompt into any agent.{% endcall %}

Appearance

{{ step_errors('appearance') }}
Night mode {% for t, label in [('light', 'Off'), ('dark', 'On'), ('system', 'Follow the system')] %} {% endfor %}
Date format {% for key, spec in date_formats.items() %} {% endfor %}

How dates read and are typed everywhere in the app: lists, forms, reports. Your files and exports always keep YYYY-MM-DD.

Your own look: {{ 'in use' if theme_file.active else 'not in use' }} {% if theme_file.active %}

In use: {{ theme_file.path }} sets colours and fonts. Delete the file to go back to the default look.

{% if theme_file.problems %}
    {% for line, problem in theme_file.problems %}
  • Line {{ line }}: {{ problem }}
  • {% endfor %}
{% endif %} {% else %}

Not in use. Ask your AI tool to write {{ theme_file.path }}, for example "make my Hermit CRM blue"; Help > Settings lists what you can change.

{{ theme_example }}
{% endif %}

Support Hermit

Hermit CRM is free and open source, and stays that way. A tip on Ko-fi supports its development.

Support Hermit on Ko-fi

Opens {{ support_url | replace('https://', '') }} in your browser; Hermit CRM sends nothing to Ko-fi.

About

{%- set up = upgrade_banner() %}
VersionHermit CRM v{{ about.version }}{% if up.kind == 'restart' %} — v{{ up.version }} is installed but this server still runs v{{ up.running }}.{% if up.can %}{{ m.upgrade_form(up) }}{% else %} {{ up.why_not }}{% endif %}{% elif up.kind == 'available' %} — v{{ up.version }} available:{% if up.can %}{{ m.upgrade_form(up) }}{% else %} {{ up.command }}{% if up.why_not %} ({{ up.why_not }}){% endif %}{% endif %}{% elif about.update_note %} ({{ about.update_note }}){% endif %} Changelog
Data folder{{ about.data_dir }}
Data format{% if about.data_format is none %}unknown{% else %}{{ about.data_format }}{% if about.data_format != about.latest_format %} (this Hermit CRM writes {{ about.latest_format }}; run hermitcrm migrate){% endif %}{% endif %}
HelpAll help topics, or hermitcrm help
{# ======================================================== Email & calendar #} {{ group_start('mail') }}

BCC capture

{{ status(state.bcc) }}{{ how('/help/settings#bcc-capture') }}
{% if state.bcc and bcc_form.address %}

BCC {{ bcc_form.address }} when you email a prospect, or forward a thread to it, and the mail is logged on the right company.

{% else %}

Put a tracking address in BCC when you email a prospect, and the mail is logged on the right company. {%- call m.hint('bcc-gmail-help', 'Which address to use with Gmail') %} With Gmail, you+crm@gmail.com reaches your own inbox, so no second account is needed.{% endcall %}

{% endif %}
{% if last_run %} {% if last_run.ok %}

Last import {{ ui_datetime(last_run.at) }}: {{ last_run.summary }}

{% else %}

Last import {{ ui_datetime(last_run.at) }} failed: {{ last_run.error }}

{% endif %} {% else %}

No import has run yet.

{% endif %} {% if inbox_alert and last_run and last_run.ok %}

{{ inbox_alert }}

{% endif %}

Mail it cannot place waits on Home, under To file.

{% if gmail_filter %}
{% endif %}
{{ step_errors('bcc') }}
{{ 'Change mailbox' if state.bcc else 'Set up the mailbox' }}
{% for key, p in mail_providers.items() %}

{{ p.label }}: {{ p.note }}{% if p.password_url %} Create the app password.{% endif %}

{% endfor %}
{% if os_store %}{% endif %}

{% if os_store %}Stored in .secrets.toml (mode 600) or {{ os_store_label }}, never in config.toml.{% if os_store == 'secret-service' %} A keyring is locked while you are logged out, so an import that runs then cannot read the password: keep it in .secrets.toml if the machine imports without you.{% endif %}{% elif is_windows %}Stored in .secrets.toml, never in config.toml: plain text, readable only by your user account, never committed to git.{% else %}Stored in .secrets.toml (mode 600), never in config.toml: plain text, readable only by your user account, never committed to git. No system keyring was found {%- call m.hint('keyring-help', 'How to use a system keyring') %} Install libsecret-tools and run a Secret Service such as GNOME Keyring or KWallet to use one.{% endcall %}.{% endif %}

{% if gmail_filter %}

Gmail filter

Keeps these copies out of your inbox. In Gmail: Settings → Filters → Create new filter.

{{ gmail_filter }}
{% endif %}

Sending

{{ status(send_form.enabled, 'on', 'off') }}{{ how('/help/sending') }}

Send approved outreach from the Outreach page, now or spaced over the working day, from a mailbox of its own. {%- call m.hint('send-help', 'What goes out') %} You still approve every message. Mail is plain text only: no links, no tracking pixel.{% endcall %}

Use a separate mailbox, ideally on its own domain: cold email can hurt the reputation of the domain it is sent from. 50 a day is the plan; a new mailbox starts at 10 and steps up over the ramp below.

{% for problem in send_problems %}

Config: {{ problem }}

{% endfor %} {% if send_cap_note %}

{{ send_cap_note }}

{% endif %} {% if send_form.enabled %}

{{ send_status.line }}

{% if send_status.missing %}

Not ready: {{ send_status.missing }}

{% endif %}

Scheduled sending: {% if send_job.send_installed %}on, the queue is checked every few minutes while this computer is awake.{% elif send_job.send_elsewhere %}the job on this computer belongs to {{ send_job.send_elsewhere }}.{% else %}off: approved email waits in the queue until you turn it on.{% endif %}

{% endif %}
{% if send_form.enabled %}
{% if send_job.send_installed %} {% else %}{% endif %}
{% endif %}
{{ step_errors('send') }}
{{ 'Change the sending settings' if send_form.enabled else 'Set up sending' }}
{% for key, p in send_providers.items() %}

{{ p.label }}: {{ p.note }}{% if p.password_url %} Create the app password.{% endif %}

{% endfor %}
{% if os_store %}{% endif %}

Stored in {% if os_store %}.secrets.toml (mode 600) or {{ os_store_label }}{% else %}.secrets.toml (mode 600){% endif %}, never in config.toml. One app password covers sending and the reply check (IMAP). {% if os_store == 'secret-service' %}A keyring is locked while you are logged out, so scheduled sending then cannot read it: keep it in .secrets.toml if the machine sends without you.{% endif %}

When and how much

Days {% for d in send_weekdays %} {% endfor %}

More than 150 a day for more than 1-2 days in a row is strongly discouraged: after two such days the third is held at 150. German and Austrian law ask for consent to advertising email, B2B included; that is yours to judge (not legal advice), so a country can be switched off above.

Calendar

{% if state.calendar %}{{ status(true) }}{% endif %}{{ how('/help/calendar') }}

{% if not state.calendar %}Optional. {% endif %}Logs past meetings with people at your companies.

{% if calendar_last_run %} {% if calendar_last_run.ok %}

Last meeting import {{ ui_datetime(calendar_last_run.at) }}: {{ calendar_last_run.summary }}

{% else %}

Last meeting import {{ ui_datetime(calendar_last_run.at) }} failed: {{ calendar_last_run.error }}

{% endif %} {% else %}

No meeting import has run yet. Meetings that match no company wait on Home, under To file.

{% endif %}
{{ step_errors('calendar') }}
{{ 'Change calendar address' if state.calendar else 'Add your calendar' }}

Google: Settings → your calendar → Integrate calendar → Secret address in iCal format. Treat it like a password.

{# ================================================================ Your CRM #} {{ group_start('crm') }}

Playbook

{{ status(playbook_status.ready, 'ready', 'not ready') }}{{ how('/help/playbook') }}

What you sell, to whom, and how you write (MESSAGING.md), one part per approach. {%- if playbook_status.problem %} {{ playbook_status.problem }}. {%- elif playbook_status.ready %} Ready for the outreach bot: {{ playbook_status.ready | join(', ') }}. {%- else %} No approach is ready for the outreach bot yet.{% endif %} {%- set waiting = playbook_status.approaches | reject('in', playbook_status.ready) | list %} {%- if waiting and playbook_status.ready %} Not ready yet: {{ waiting | join(', ') }}.{% endif %}

Open the playbook

Learning from your edits (Learning page): who reads the evidence and proposes lessons.

Fields

{{ how('/help/settings#fields') }}{{ describe('fields') }}

Fields of your own, on top of the built-in ones, described in fields.toml. {%- call m.hint('fields-values-help', 'Where the values live') %} Their values live in your records' front matter.{% endcall %}

{{ step_errors('fields') }} {% if custom_field_defs %}
{% for f in custom_field_defs %} {% endfor %}
keylabeltypeonshown in
{{ f.key }}{{ f.label }}{{ f.type }} {{ f.applies_to }}{{ f.show_in | join(', ') }}
{% else %}

No fields of your own yet.

{% endif %}
Add a field
Shown in {% for view in ['detail', 'board', 'companies', 'contacts', 'messages'] %} {% endfor %}
Edit fields.toml directly

Removing a field only hides it: the values stay in your records and return with the field.

Drafts

{{ how('/help/messages') }}{{ describe('messaging') }}

Which fields fill the team-size sentences in outreach drafts; with neither set, those sentences leave a line for you to fill in. {%- call m.hint('draft-fields-help', 'Using your own fields in drafts') %} Any field of your own can also be used in a draft as {{ '{' }}key{{ '}' }}.{% endcall %}

{{ step_errors('messaging') }}

Task types

{{ describe('task-types') }}

Your own labels for tasks and next steps, used as filters. {%- call m.hint('task-types-help', 'What renaming and deleting do') %} Renaming a type renames it on every task; deleting one leaves it on its tasks, in grey.{% endcall %}

{{ step_errors('task-types') }}
{% for t in task_types_form.rows %} {% endfor %}
{{ m.type_chip(t.name) }} {% if not loop.first %}{% endif %} {% if not loop.last %}{% endif %}

Deal stages

{{ how('/help/settings#deal-stages') }}

The steps a deal goes through, in order: the Pipeline columns, the filters and the funnel follow this list. {%- call m.hint('stage-roles-help', 'What each role means') %} Each stage has a role that tells Hermit what it means. open is a column on the board (the first one is where new companies start and where a requalified one returns). won and lost end a deal (lost needs a reason). closed is any other end, such as disqualified. parked is out of the pipeline for now, with a date to come back.{% endcall %}

{{ step_errors('stages') }}
{#- Enter in a name box submits the form's first button: make that Save, not a row's Delete. -#} {% if stages_form.confirm %} {% set ask = stages_form.confirm %}
{{ ask.count }} {{ 'company is' if ask.count == 1 else 'companies are' }} still in {{ ask.names | join(', ') }}. Move {{ 'it' if ask.count == 1 else 'them' }} to
{% endif %}
{% for r in stages_form.rows %} {% endfor %}
stagerolevaluedcompanies
{{ r.count }} {% if not loop.first %}{% endif %} {% if not loop.last %}{% endif %}

Outcomes

{{ how('/help/settings#outcomes') }}{{ describe('outcomes') }}

One per line, in order: a detected reply counts as the first, silence past the message window as the last.

{{ step_errors('outcomes') }} {% if outcomes_offer %}

New: an interested outcome, for a reply that shows interest. It goes in the middle, so replies and silence count as before: {{ outcomes_offer | join(', ') }}. {%- call m.hint('interested-help', 'What the interested outcome does') %} Set it when you log such a reply and the Outreach page lists it under Replies to interest, no AI needed.{% endcall %}

{% endif %}
{# ==================================================== Data (backup, export) #} {{ group_start('backup') }}

Backup

{{ status(state.backup) }}How to roll back
{{ step_errors('backup') }}

On this computer

{%- if lb.level == 'ok' and lb.age_minutes is not none -%} Last backup {{ 'just now' if lb.age_minutes < 1 else lb.age_minutes ~ ' min ago' }} ({{ lb.size }}). {%- elif not lb.last_ok -%} No backup yet. {%- else -%} {{ lb.summary }} {%- endif -%} {% if lb.last_ok and not schedule.backup_installed %} It does not repeat on its own yet.{% endif %}

Every few minutes, to a repository outside this folder that can only grow, so any version can be put back.

{% if schedule.backup_elsewhere %}

This computer's backup job is set up for another folder, {{ schedule.backup_elsewhere }} (there is one per computer). To back up this folder instead, run hermitcrm --data {{ about.data_dir }} schedule install; the other folder is then no longer backed up.

{% elif not schedule.backup_installed or not lb.last_ok %}
Makes a first backup now, then one every 5 minutes in the background.
{% endif %}
Where it is kept

In {{ lb.dest }}, outside your CRM folder, so deleting the folder keeps the backup. For another disk or a synced folder, set backup_dir in config.toml.

{% if lb.lines[1:] %}{% endif %}

Online copy (optional){% if state.remote %} {{ status(true) }}{% endif %}

{% if remote_url %}

Pushed to {{ remote_url }} after every change.

{% else %}

The backup above is on the same disk as your data; push to a git remote after every change for a copy that survives a lost laptop.

{% endif %} {% if remote_warning %}

{{ remote_warning }}

{% else %}

The repository must be PRIVATE on GitHub, GitLab or Bitbucket: it holds your contacts and mail.

{% endif %}
{{ 'Change the repository' if remote_url else 'Set up an online copy' }}

Export everything

{{ how('/help/import#export') }}

Your companies, contacts, interactions, tasks and stage history as a zip of CSV files, for a spreadsheet or another CRM.

Also from a terminal: hermitcrm --data {{ about.data_dir }} export (add --format json for one JSON file).

{# ================================================================ Advanced #} {{ group_start('advanced') }}

Set up from a terminal; this tab shows what is running and the commands to copy.

Schedule

{{ status(schedule.installed) }}{{ how('/help/settings#schedule') }}

A daily job runs hermitcrm sync --apply: the BCC import, then the calendar import.

What is installed, and the install command

Daily at 07:00; --at HH:MM changes the time, and without --serve the web app is left to you.

{{ schedule.install_command }}

Access from your phone

{% if access.on_network %}{{ status(true, 'on') }}{% endif %}{{ how('/help/settings#access') }}
{% set no_password %}

Hermit CRM has no password: anyone who can reach that port can read and write your CRM. Use a network you trust or a private one (Tailscale, WireGuard), not public Wi-Fi.

{% endset %} {% if access.on_network %}

The app answers on your network: open {{ access.lan_url }} on a phone on the same Wi-Fi.

{{ no_password }} {% else %}

The app is bound to {{ access.host }}, which only this machine can reach.

How to turn it on
{{ access.serve_command }}

Then open {{ access.lan_url }} on the phone. To make it permanent, set host = "0.0.0.0" in config.toml.

{{ no_password }}
{% endif %}

Access from an AI app (MCP)

{{ how('/help/ai-agents') }}

Lets Claude Desktop, ChatGPT desktop or Cursor on this computer read and write this CRM. {%- call m.hint('mcp-help', 'What an AI app can do here') %} It can show your pipeline and the replies you owe, brief you before a meeting and log the call afterwards, and make Hermit yours (fields, dashboards, routines, the look), each change one commit you can undo.{% endcall %}

The config to paste

Paste this into your AI app's MCP config; it starts the server itself, over stdin and stdout, so the app must run on this computer.

{{ access.mcp_config }}

The command on its own: {{ access.mcp_command }}

Check the installation

hermitcrm --data {{ about.data_dir }} doctor

Add --online to try the IMAP login as well.

{% endblock %}