# Hermit CRM docs > How every part of Hermit CRM works: pipeline, contacts, mail capture, reports, the CLI, the MCP server and how your AI agent can change it. For version 1.0.0, 2026-10-27. The same text as https://hermitcrm.io/docs/ and as `hermitcrm help ` in the app. ## Pipeline The board at `/pipeline`: one column per open stage, cards you can move with a dropdown, closed companies in lists below. The stages are yours to name and order (Settings > Deal stages); this page describes the defaults. ### Columns and cards The open stages are columns. By default there are four: **prospect**, **engaged**, **discovery**, **offer**. A column with no companies is drawn narrow so they fit on one screen. A card shows the company name (a link), days in the current stage, the country, the last touch (`email out 2026-09-14 (jane-doe)`), the next step with its due date (red when overdue, struck through when done) and the monthly value when set. The stage dropdown on a card saves on change; choosing **lost** sends you to the company page to give a reason first. Cards are ordered by next-step due date (earliest first, no date last), then by last touch (most recent first). ### Follow-up radar The **Follow up** list above the filter bar is not part of the board. It is the companies where a conversation is waiting, in two groups: - **You owe a reply**: the latest message in the company's history is one they sent, and it is at least `followup_reply_days` old (default 1). - **Waiting on them**: the latest is a message you sent, nothing has been planned, and it is at least `followup_nudge_days` old (default 5). Replies you owe come first, then your unanswered messages; inside each group the oldest is on top. Both thresholds are in `config.toml` in your data folder. Each row shows the evidence: the channel, direction and date of that interaction (a link to it), and the first words of the message in quotes. What you wrote starts with **you:**, so your own words do not read as theirs. A pasted mail header ("Name", "10:12 AM (1 minute ago)", "to Sam") is skipped when picking the words. What counts, and what does not: - A **note** is a memo and never counts as contact, so adding one does not clear a row. - A **meeting** is not a message you sent, so it never makes a "waiting on them" row. - A task for later silences "waiting on them" for that company until it is due. It does **not** silence "you owe a reply": a reply you owe is owed. - Closed and parked companies are not on the list. If a row is wrong because the message was logged the wrong way round, press **I sent this** (or **They sent this**) on the row. It flips the direction of that one interaction and nothing else; the text is kept exactly as it was, and **Undo** in the history brings it back. A card on the board whose company is on the radar carries a small badge (**reply owed 4d**, **no answer 25d**) that jumps back to the list. The board's own order is unchanged. ### Closed and parked Below the board, every other stage is a collapsed list: by default **won**, **lost**, **disqualified** and **temp-disqualified**, with the reason and, for parked companies, the date they come back to the first open stage (`requalify_on`). That return happens by itself: on any page view, a parked company whose date has arrived goes back to prospect in one commit. ### Filter bar The bar above the board has one control per column: the **columns** list picks which stages show; the other controls filter the cards. Text boxes take a small syntax: `text` contains, `!text` does not contain, `=text` equals, `>x` and `` is the account page with its fields, contacts, drafts, merge and timeline. ### The table Columns: name, country, stage, source, any field of your own that asked for this table, tags, last touch, next step, due, plus website and LinkedIn links. The search box in the nav matches company name, tags, contact names and contact emails. Each column has a filter control (see the `?` next to Filter for the syntax) and sort arrows. Parked companies (by default the temp-disqualified ones) are hidden until you click "Show temp disqualified (N)" or filter on that stage; the link carries the name of your parked stage. A company in a stage that Settings no longer lists shows in grey. New companies come from `/companies/new` (name is required; website, LinkedIn, country, source, stage, value, next step and tags are optional; a first note, if you write one, becomes a company note), from [Import](https://hermitcrm.io/docs/#import), or from the BCC and calendar imports on the Settings page. ### The sample account A new folder can load one made-up company to look at: **Look at a sample account** on the empty home page or on Getting started, or `hermitcrm sample add`. It is Northwind Robotics, with two people, a message and its reply, a meeting, a deal at offer with its stage history, a next step and tasks. Its `company.md` carries `sample: true`, and a bar at the top of every page says it is there. The Getting started steps do not count it. It comes with two things from [Make it yours](https://hermitcrm.io/docs/#adjust), so you see what you can build before you ask for anything: the dashboard **Monday review**, pinned in the sidebar (`dashboards/monday-review.toml`), and the routine **Nudge quiet threads** in `routines.toml`, paused, so it does nothing until you turn it on. Neither replaces a file or a routine of the same name you already have. `hermitcrm init --demo` writes the same two. **Remove it** in that bar (or `hermitcrm sample remove`) lists what goes and deletes it in one commit, the dashboard and the routine included. Whatever you changed stays: a dashboard you edited, the routine once you edited or turned it on, and your own routines next to it. Only companies with `sample: true` can be removed this way; delete that line by hand and the company is yours to keep. Hermit CRM has no other way to delete a company: you disqualify it instead. ### The company page - The header, two lines: one-liner, website and LinkedIn links, stage with days in stage, last touch, and any field of your own; then **Enrich** (when an AI CLI is available) and **Fetch from URL**. Country is in the edit form below, not the header. - **Disqualify** and **Temp disqualify** (with a reason and, for temp, an "until" date); **Requalify** brings a disqualified company back to prospect. The menu offers every stage whose role is closed or parked, so with stages of your own the buttons carry their names. - The next step (the open task due first) with its status tag, **Done** / **Reopen**, the Google Calendar link and a link down to all tasks. - **Company**: every field in an edit form. There is no notes box: company text is dated company notes, listed under **Company notes** below. Setting the stage to lost needs a reason; leaving lost, disqualified or temp-disqualified clears it. Below the form: slug, created, updated, stage changed, and the stage history collapsed. - **Contacts** with title, role, email and LinkedIn, a **log** link to each person's Log an interaction form (an interaction is always with a person, so the company page has no form of its own), and a New contact link. Under the list, **Company notes**: dated notes about the company as a whole, with an **Add company note** link; each contact row shows how many notes that person has. - **Tasks**: one list for the company and its people, open ones by due date (the first is labelled **next step**), then the done ones with the day they were done. **Done** / **Reopen** / **Delete** on each, and **Add task** with a "for" list: the company or one of its people. - **outreach** on each row of the contacts table opens [Outreach](https://hermitcrm.io/docs/#outreach) with that person ready to generate (see [Contacts](https://hermitcrm.io/docs/#contacts)). - **Merge**: pick another company to compare and merge into this one. - **Timeline**: every interaction across all contacts, newest first, bodies collapsed (a note shows its text). Edits made outside the app (by hand or by an agent) show up on the next page view: the company's folder is re-read for every request. ### Tasks and the next step A company has as many **tasks** as you like, each with an optional due date. They live on the company, or on one of its contacts when the thing you owe is owed to a person. A contact's tasks show on their own page and in the company's list with their name. The **next step** is not a separate thing you pick: it is the company's open task due first (undated tasks come after dated ones). That is what the board card, the Companies table, PIPELINE.md, meeting briefs and the follow-up radar show. To change what is next, change a due date or tick the task off. Adding a task never replaces another one. Files written before this rule keep their `next_step` field; it is read as one more task on the list, and **Done** and **Delete** work on it like any other. Ticking a task off records the day (`done_on`), shown on the company page and filterable on the [Tasks](https://hermitcrm.io/docs/#tasks) page. Deleting a contact takes their tasks with them, and says how many are open before it does. Merging two records keeps both lists: a task is work you still owe, so there is no side to pick. ### Stages The stages are a setting: you name them, order them and give each a role (Settings > Deal stages, or `hermitcrm help adjust-stages`). These are the defaults. `prospect`, `engaged`, `discovery`, `offer` are open; `won`, `lost` and `disqualified` are closed; `temp-disqualified` is parked: off the board and the table, back to prospect by itself on `requalify_on`. Every stage change appends to `stage_history` and sets `stage_changed` to today. The first interaction logged on a prospect moves it to engaged (the first open stage moves to the next one, when that is open too). Before format 4 the engaged stage was called `reached-out`; migration 4 renamed it, and `reached-out` is still accepted as input (imports, CLI). ### Country and language `country` is an ISO 3166-1 alpha-2 code (`UK` and `USA` are accepted and stored as `GB` and `US`). It also picks the draft language: German for DE, AT, CH and LI, Dutch for NL, French for FR, LU and MC, English otherwise. For **BE** the record decides between Dutch and French. Evidence, strongest first: previous mails and messages (their replies weigh most, then yours), a `/nl/` or `/fr/` website (or `nl.`/`fr.` subdomain), a postcode outside Brussels (Flanders: Dutch; Wallonia: French; Brussels 1000-1299 counts for neither), then the language of contact titles, notes, company notes and the product line. One side needs at least twice the weight of the other; otherwise the drafts are English. To force a language, add a `/nl/` or `/fr/` website or a note with the address. Related: [Pipeline](https://hermitcrm.io/docs/#pipeline), [Contacts](https://hermitcrm.io/docs/#contacts), [Interactions](https://hermitcrm.io/docs/#interactions), [Merge](https://hermitcrm.io/docs/#merge), [Enrich](https://hermitcrm.io/docs/#enrich), [Data format](https://hermitcrm.io/docs/#data-format) ## Contacts `/contacts` lists every person across companies; a contact page holds the fields, this person's interactions, notes and a button that opens Outreach for them. ### The table Columns: name, company, title, email (a mailto link), LinkedIn, last touch and interaction count, with a filter control per column and sort arrows. The search box matches name, email, title and company. ### The contact page `/companies//contacts/`: - Header: company, title, role, email, phone, LinkedIn, and **Enrich** when an AI CLI is available. - **Contact**: first name, last name (at least one required), title, role (`champion`, `decision-maker`, `influencer`, `gatekeeper` or none), email (stored lower case), phone, LinkedIn. There is no notes field: what you know about a person is a **note** on their timeline. - **Interactions** of this contact, newest first, notes included. - **Log an interaction** for this contact: an email, LinkedIn message, call, meeting or note. - **Tasks** you owe this person. - **Generate outreach** (below). - **Merge** another contact of the same company into this one (only shown when the company has more than one contact). - **Delete contact** (asks first). Their interactions stay on the company with the contact cleared, so the record survives; git keeps the file's history. New contacts come from the company page's New contact link (`/companies//contacts/new`), from Import in contacts mode, and from the BCC and calendar imports (a new person at a company whose domain is known). ### Keep in touch The follow-up radar and the silent list are about deals. **Keep in touch** on Home is about everyone else: people you have not spoken to in `keep_in_touch_days` days (default 180, about six months) who are not in an open deal. It is a read-only list, longest quiet first, eight on Home; `hermitcrm keep-in-touch` (and the `keep_in_touch` tool for an AI client) prints all of them. - **Last touch** is the latest interaction logged against that person, in either direction: an email, a message, a call or a meeting. Notes do not count, and neither does an interaction logged against the company with no person on it. - **Left out**: people at a company in an open stage (prospect, engaged, discovery, offer, or whatever your open stages are), so the list holds customers, lost and disqualified deals and parked companies; anyone tagged `do-not-contact`, on the company or on the person; people you never had any interaction with; and the sample account. - Logging anything with a person takes them off the list. - Change the threshold in `config.toml` (`keep_in_touch_days = 90`), move or hide the section with `layout.toml` (`keep-in-touch`, see [Make it yours: layout](https://hermitcrm.io/docs/#adjust-layout)). ### Tasks The contact page lists the tasks you owe this person, with **Done**, **Delete** and a form to add one. They also show in the company's task list, and when one of them is the company's task due first, it is the company's next step. ### Generate outreach The **Generate outreach** button in the action row opens [Outreach](https://hermitcrm.io/docs/#outreach) with this person ready to draft: channel, draft type, signal, a language, a box to add a note, and the notes the draft will be written from. Nothing runs until you press **Generate**. When the person already has an open draft, the button says **Open outreach draft** and goes to its card. The no-AI **Message drafts** box that used to sit here is gone; its signal, language picker and note box moved to that panel, and what you write there is kept as notes. The contact's **draft language** (set on the contact, say for a German speaker at a Dutch company) is where the panel starts, else the language of the company's country. The wording of reply and follow-up templates lives in the package's `default_messages.toml`; a `messages.toml` in the data folder overrides any part of it, and `MESSAGING.md` there is your playbook. Related: [Companies](https://hermitcrm.io/docs/#companies), [Interactions](https://hermitcrm.io/docs/#interactions), [Messages](https://hermitcrm.io/docs/#messages), [Merge](https://hermitcrm.io/docs/#merge) ## Interactions An interaction is one touch with a person: an email, LinkedIn message, call, WhatsApp message or meeting, in or out, with the text kept verbatim. A **note** is an interaction too: a memo about the person, with no direction, that shows on their timeline but never counts as contact made (it does not move last touch, a prospect's stage, follow-ups, outcomes or reports). ### Logging one Notes may also be about the whole company: pick **whole company** in the contact dropdown, or use **Add company note** (on the company page and on every contact page). Only a note can be left without a contact. A company note shows on the company page and, under **Notes elsewhere**, on every contact's page, as do the notes on the company's other contacts (the newest five of each, with a link to the rest), so nothing a colleague knows is hidden from you. The company form has no notes box: company text is company notes, dated like any note, and outreach drafts cite them like any other note. An interaction is always with a person, so the **Log an interaction** form sits on every contact page (the "log" link in a company's contact list goes there), and on its own at `/companies//interactions/new` where you pick the contact (the "log interaction" links on the Calendar open it). Fields: channel (a dropdown: `email`, `linkedin`, `call`, `whatsapp`, `meeting`, `note`, default linkedin), direction (`out` or `in`, default out; hidden for a note), date and time (default now, editable), subject, outcome and the body. The body is the pasted message or the call notes; it is stored byte for byte and never rewritten. Logging an interaction (by hand or from the BCC and calendar imports) on a company in **prospect** moves it to **engaged**, in the same commit; a note does not. Other stages are left alone. Saving redirects to the contact's page with the timeline scrolled to the new entry. Each entry has an edit link (`/companies//interactions//edit`) where every field including the date can change; a date change renames the file. ### Outcome `outcome` is one line: what came of it. The choices are configured on the Settings page (`outcomes` in `config.toml`, default `successful`, `interested` and `unsuccessful`). Set `interested` on a reply that shows interest: the Outreach page lists it under Replies to interest. For a sent message the outcome decides its status on the Messages tab: an explicit outcome wins; otherwise a later inbound interaction from the same contact (or a company-level one) counts as the **first** configured outcome; otherwise, once the message is older than `message_window_days` (default 14), it counts as the **last** one; until then it is unknown. Files from before format 3 carried a separate `result` key; migration 3 folded it into `outcome`. ### Where interactions come from - **manual**: the form. - **bcc-import**: mail you BCC'd or forwarded to the tracking address, logged at the contact with that email; the quoted thread is cut once at import. - **calendar-import**: past meetings from the calendar feed, one `meeting` interaction per external attendee, subject from the event title, body from its description. Imports carry a `message_id` so a rerun never logs the same mail or meeting twice. Mail and meetings that match no company wait on Home under **To file**. ### To file (the review queue) Home shows a **To file (N)** list under *What needs doing* whenever the imports left something they could not place (while the walkthrough is still your home page, it shows there). Each item has the date, kind, direction, person and subject, the reason (personal address, unknown domain, several matching companies), the text collapsed, and a form to **Log at company** (a company slug or name, legal suffixes like GmbH ignored, else the company that has the mail's domain; a name that finds neither creates a new company, with the mail's domain as its website unless it is a personal address; the contact is found by email, else a same-named contact gets the email, else it is created) or **Discard** (remembered in `inbox/discarded.tsv`). The count shows on Home in the nav. The import status and the **Import now** buttons are under Settings, BCC capture and Calendar; the red `!` on Settings means the last import failed or is two days old, and jumps to the one that did. ### Files and ids `companies//interactions/.md` with the id `YYYY-MM-DDTHHMM---` (a collision gets `-2`, `-3`). The commit is `interaction: `. Related: [Messages](https://hermitcrm.io/docs/#messages), [Companies](https://hermitcrm.io/docs/#companies), [Contacts](https://hermitcrm.io/docs/#contacts), [Settings](https://hermitcrm.io/docs/#settings), [Data format](https://hermitcrm.io/docs/#data-format) ## Messages `/messages`: every message you sent, newest first, with what came of it. ### Fields of your own in a draft Every [field you defined](https://hermitcrm.io/docs/#settings) is a slot the templates can use by its key: a field `segment` is `{segment}`. A field with no value yet renders as its label in square brackets, like the other things only you can fill in, so a draft never goes quietly blank. A field can be kept out of the templates entirely with `messaging = false` in `fields.toml`, and a field whose key is already a slot name (`company`, `site`, `first`, ...) is ignored rather than shadowing it. Two fields can also play the roles the shipped wording knows about: a headcount, for the sentences about team size, and a count of sales people. Pick them under Settings -> Drafts. With neither set, those sentences use their "unknown" wording, which is a line you write yourself; nothing is invented. ### What counts as a message An outbound interaction with a body, on any channel except `meeting`. Calls with notes count too; a logged interaction without text does not. ### The table Sent, company, contact, channel, country, stage, outcome, how many times that exact text was used (whitespace and case ignored), and the message itself, collapsed. Every column has a filter control and sort arrows; the search box matches the message text, the company and the contact. The totals line at the top counts each status. ### Outcome A message's status is its outcome when one was set by hand; otherwise it is derived: a later inbound interaction from the same contact (or a company-level one) makes it the first configured outcome (default `successful`); no reply within `message_window_days` (default 14) makes it the last one (default `unsuccessful`); before that it is `unknown`. Derived values are marked "(auto)". The buttons in the last column set or clear the outcome by hand (commit `interaction: outcome `; the same field the interaction form edits, so both places always agree), and "edit" opens the interaction. The outcome list and the window are set on the Settings page. The Reports page breaks message outcomes down by language, channel and the most reused texts. Related: [Interactions](https://hermitcrm.io/docs/#interactions), [Reports](https://hermitcrm.io/docs/#reports), [Settings](https://hermitcrm.io/docs/#settings) ## Playbook `MESSAGING.md` in your data folder: what you sell, to whom, and how you write, per approach. The playbook is the one place Hermit CRM learns about your business. The outreach drafts, routines and your own AI agent read it. It works for any B2B business (a service, software, an agency); every part is optional except the minimum the outreach bot needs (below, under "Ready for the outreach bot"). Fill it in under **Settings > Playbook** (`/settings/playbook`): one tab per approach, one box per part with an example beside it, a meter that shows how much is filled, and a Save button that stays in view. Or edit `MESSAGING.md` yourself, or ask your AI agent to. ### Approaches An approach is one offer to one audience. Most people have one. If you sell different things to different companies (coaching to founders who still sell themselves, building the team for companies that already have salespeople), give each its own approach. Each approach is complete on its own: offer line, ideal customer, personas, proof, voice, channels, don'ts, disqualifiers and lessons. - **New approach** starts empty or as a copy of another one; **Duplicate this approach** copies everything, so you only change what differs. - The outreach bot picks the approach a company fits (see "Which approach a company gets"), and drafts with that approach's parts only. - An approach that is not filled in yet is simply not used; the others are. For a separate business with its own pipeline, use a second data folder (`hermitcrm init`) instead: one folder keeps one pipeline and one set of message statistics. ### Two halves - **The front matter** (the YAML between the two `---` lines at the top) is for Hermit CRM itself: per approach its name, offer line, ideal customer, hard disqualifiers, personas and, if you set it, its message frameworks. The app reads it to pick and sort prospects; an AI is never given it as instructions. - **The body** (everything below) is for people and AI: the general notes on slots and languages, then one `# Approach: ` block per approach with the rest. ### The front matter ```yaml --- approaches: - name: Booking software offer_line: "We build booking software for clinics." icp: industries: [healthcare, dental] countries: [NL, BE, DE] # two-letter codes ranges: # your own company fields: [min, max] employees: [5, 200] funding: [bootstrapped, seed] signals: [new-location, hiring-reception] # positive signals disqualifiers: - tag_present: [customer, do-not-contact] personas: owner: {titles: ["Owner", "Practice Manager", "Managing Director"]} ops: {titles: ["Operations Manager", "COO"]} --- ``` | Key | What it holds | |---|---| | `name` | The approach's name, unique in the file; its body block is `# Approach: `. | | `offer_line` | What you sell, in one sentence. One per approach: a list is refused. | | `first_step_offer` | Optional. The first step a reply to interest offers, in one sentence ("The first call is free and all about you, without a sales pitch."). Left out of the draft when empty. | | `icp` | The ideal customer: `industries`, `countries`, `funding`, `signals` (lists of short words) and `ranges` (below). Leave any of them empty. | | `disqualifiers` | Hard reasons a company is not a prospect for this approach (below). | | `personas` | Per buyer type, the job `titles` that belong to it. Optional `pains`, `cares` and `ignores` lists. | | `frameworks` | Optional. The message framework this approach writes first messages with, and the ones to try against it (below). Left out, the approach uses `house`. | #### Ranges on your own fields Hermit CRM has no built-in headcount or team size: what a company "is" lives in the fields you defined yourself ([Adjust fields](https://hermitcrm.io/docs/#adjust-fields), `fields.toml`). `ranges` maps one of those company fields to `[min, max]`, as many as you like: a headcount, a revenue band, the number of salespeople, the number of locations. A number field is read as it is; a text field by the first number in it, so `~13` counts as 13. `hermitcrm check` warns when a range names a field the folder does not have. #### Not a prospect Write every reason a company is not a prospect for this approach in its `## Disqualifiers` section, one per line ("they already have a Head of Sales", "they work with an agency", "public sector"). The AI weighs those against what it finds and says why; you decide. One check the app makes itself, without AI: `tag_present` leaves out every company with one of these tags (`customer`, `do-not-contact`). #### Which approach a company gets The ideal customer decides, on what Hermit CRM knows: your ranges, the country, and industries as tags. An approach whose range or countries the company falls outside, or whose tags rule it out, is left out. Of the rest, the one with the most matches wins. An empty field cannot decide: when two approaches stay equal, the bot asks you, and says what was missing ("no employees"). Funding and positive signals are for research, not for this choice. #### Opt-out line (optional) Beside `approaches:`, the front matter may hold one `opt_out` for the whole folder: the line the outreach bot adds at the end of a cold first email. One sentence for every language, or one per language code, falling back to `en`: ```yaml opt_out: en: "Not for you? Say so and I won't write again." de: "Passt nicht? Sag kurz Bescheid, dann schreibe ich nicht mehr." ``` Without it, no such line is added. #### Personas A contact's title picks the persona: whole words, any case, and the longest matching title wins, so `Head of Sales Operations` is not taken for `Head of Sales`. A title no persona lists gets no persona. #### Frameworks A framework is a recipe for a first message: its parts, how long it may be and what it asks. `frameworks` is optional. Without it an approach uses `house`, the recipe first messages have always used, and nothing changes. ```yaml approaches: - name: Booking software frameworks: default: trigger-tension-trust testing: [house] answers: {deal: large, buyer: exec, volume: low, research: five, tone: plain} ``` - `default` is one framework id. The outreach bot writes this approach's first messages with it. - `testing` is a list of up to two ids to try against the default: none twice, and not the default itself. Split tests and compare mode use it; they come in a later version. - `answers` records the answers to the setup questions the default was picked from. The setup questions come in a later version; until then `hermitcrm check` only makes sure each answer is one it knows: `deal` (small, mid, large, xl), `buyer` (exec, lead, staff), `volume` (low, mid, high), `research` (minute, five, fifteen) and `tone` (plain, warm, blunt). The ids are those of the framework library: `default_frameworks.toml` in the Hermit CRM package, with any framework you add or change in `frameworks.toml` in your data folder on top. A help page that describes each framework comes later. `hermitcrm check` names an id the library does not have, and a key or answer it does not know. ### The body Each approach block holds these `## ` headings. A comment under each heading says what goes there; the text you write below it is what counts. | Heading | What goes there | |---|---| | `## Offer` | The outcome a customer gets, how an engagement works, what you do not do. | | `## Personas` | Per persona: the top 2-3 problems, what they care about and ignore. | | `## Proof` | Customer results, one per line starting with `- `. Drafts may cite only these. | | `## Why you` | Why you rather than doing it themselves, hiring, another provider or nothing. | | `## Voice` | Tone, words you use and avoid, sign-off, formal or informal address. | | `## Channels` | Per channel: what a first message is for, its length, what it asks. | | `## Don'ts` | What a message must never do. | | `## Disqualifiers` | Other reasons a company is not a prospect, for the AI to weigh. | | `## Lessons` | `### Worked`, `### Didn't work` and `### Still testing`: what you tried. Drafts follow what worked and avoid what did not. | Write them in any language; a draft is still written in the contact's language. ### Ready for the outreach bot An approach is ready when it has an offer line, something in the ideal customer profile, one persona with titles and one proof line. The outreach bot runs when at least one approach is ready, and uses the ready ones only. `hermitcrm playbook` (or `hermitcrm playbook status`) says per approach what is filled and what is missing; Settings > Playbook shows the same. ### Draft it from my website or a document On Settings > Playbook, give your website address, upload a PDF, Word (.docx) or text file (a sales deck, a one-pager), or paste text, and click **Draft it**. Hermit CRM reads the website's home page and up to four pages of the same site that describe the business (About, Services, Pricing and the like). Your AI CLI (the one under Settings > AI, run without any tools) then proposes every part of the approach from that text alone. The proposal fills the form; nothing is saved until you click Save. It never invents proof: a result your text does not state stays empty. ### Learn my voice from my sent emails When the CRM has emails you sent (logged by hand, by BCC capture or the extension), **Learn my voice from my sent emails** gives your most recent ones (the quoted thread below each left out) to your AI CLI, which describes your voice in the Voice box. Check it, then Save. ### A playbook from before A data folder created before the playbook had approaches keeps its `MESSAGING.md` exactly as it is; nothing changes on its own. `hermitcrm check` says "not set up for the outreach bot" (a hint, not an error). To set it up: ``` hermitcrm playbook upgrade --dry-run # print the result, write nothing hermitcrm playbook upgrade # one commit ``` It adds the front matter with one approach at the top and that approach's block at the end, and keeps every line you had, byte for byte (your "What worked" notes included). Running it again changes nothing. Saving Settings > Playbook does the same and fills it in at once. Related: [Outreach bot](https://hermitcrm.io/docs/#outreach), [Messages](https://hermitcrm.io/docs/#messages), [Adjust messages](https://hermitcrm.io/docs/#adjust-messages), [Settings](https://hermitcrm.io/docs/#settings), [Enrich](https://hermitcrm.io/docs/#enrich), [CLI](https://hermitcrm.io/docs/#cli) ## Outreach bot Picks a batch of contacts, reads what their websites say, drafts a first message per person and checks it, answers replies that show interest and follows up on messages nobody answered; you review and approve each one. Nothing is sent that you did not approve, and until you turn on [sending](https://hermitcrm.io/docs/#sending) nothing is sent for you at all: you send and log it yourself. The outreach bot works from your [playbook](https://hermitcrm.io/docs/#playbook) (`MESSAGING.md`): what you sell, to whom, and how you write. It will not build anything until at least one approach in it is filled in (`hermitcrm playbook` says what is missing). It runs on the **Outreach** page (in the menu, next to Messages) and from the command line; both do the same steps and write the same files. ### On the Outreach page - **Who**, **Draft type**, **Channel**, **Batch size**, **Approach**: the filter (conditions separated by `;`, for example `stage=prospect; country=DE`), a first touch or a follow-up, email or LinkedIn, how many, and which playbook approach to use. Left on "the best fit per company" it picks one per company, as the command line does without `--approach`. **Preview** shows who would get a draft and why not the others; no AI runs and nothing is written. **Build queue** researches and drafts them one at a time: each card appears when it is done and the page asks for the next one by itself. **Stop** ends the batch; what was built stays. - **Tabs**: Replies to interest (first: that is where deals leak), Follow-ups, Drafts (first touches to review), Ready (to send), Sent, Skipped. - **A card**: the person and company with their links, fit, flags, what research found (with source and a confidence dot), the draft and its checks. - **Re-check** saves your edit and runs the checks on it. A check that fails never blocks you. - **Mark ready** approves the text as it is in the box (your edit and the approval are one change). A failing check stays on the card as a warning. - **What was wrong with this draft?** is one optional box (500 characters at most) under the text, for whichever button you press next. With **Re-check** or **Mark ready** it is the explainer of your edit. The AI's draft is never overwritten: it stays under **AI original**, and each save or Mark ready that changes the text adds one **tracked edit** with your explainer, shown as the words removed and added. (If the text did not change, the reason is not kept.) With **Regenerate** it is the reason the old draft was replaced. Either way it is what you can later learn from, together with which messages got a positive answer (`hermitcrm outreach edits --positive`). When you change a lot of text, a row of **quick reasons** appears as well. - **Regenerate** writes a new draft from the contact's notes as they are now, optionally with the stronger model. The old draft and its edits are kept (under "regenerated away"), with the reason if you typed one in the box above. Changes in the text box that you have not saved are not kept. **Add a note** first to steer it (see Notes, below); a note you write counts as a known fact. - **Skip** with a reason; **Copy** copies the message. - **Save to the record** shows when research found a website, LinkedIn URL or country for a field that is empty; it writes that one value and nothing else. - On a ready card: **Copy**, open your mail or LinkedIn, **Log it** (logging the message marks the item sent), or **Mark sent** without logging. With [sending](https://hermitcrm.io/docs/#sending) on, an email card also has **Send now** and **Schedule**, and the **Scheduled** tab shows the queue. - **On a contact's page**: **Generate outreach** opens this page with a panel for that person (`/outreach?contact=/`). Nothing runs until you press **Generate**. The panel has the channel and the draft type (from the history: someone who replied with interest gets a reply to interest, someone whose message went unanswered a follow-up, everyone else a first touch), the research level, the **signal** you read off LinkedIn insights, and a **language** for this card and its regenerates (the default is the contact's draft language, else the company's country's). Under the list of notes the draft will use is one box to add a note, such as one thing from their site or team. **Add note** (or Enter) saves it; pressing **Generate** saves what is in the box too, so nothing you typed is lost. The signal is saved as a note first as well. It says why when the person is left out. While an item is open, the button on their page says **Open outreach draft** and goes to its card. ### Notes What the bot knows about a person is their **notes**: the same notes you add on their page, on their timeline and on the company's. A finding is a note. - Research saves what it finds as notes on the contact, one per finding, with its source, kind, date and confidence on a last line (`Source: https://example.com/news (trigger, 2026-09, high)`). Findings with low confidence are not saved; the card says how many were left out. - The card's **Notes** panel lists the contact's notes, newest first, marks the ones the draft used, links each to its place on the timeline, and has a box to add one. Edit or delete a note where it lives, on the timeline. Company notes show there too, marked `company`; you edit or delete them on the company page. - The draft is written from the newest 20 notes of the person and the company (and at most 4,000 characters: over that, the longest go first). Company notes count like the person's: cited like any note, not background. A note you wrote counts as a known fact and never goes stale; a research note keeps its date, and one older than about 90 days never opens a message. - Notes added or changed after a draft are not in it: the card says "N notes added since this draft was written" and **Regenerate** reads them again. - The web-research AI never sees your notes; only the drafting and critic steps do, and they have no tools. - Cards made before notes were shared save the findings they showed as notes the first time you regenerate or mark them ready. Sent and skipped cards are left as they are. ### Replies to interest Someone answered and wants to know more. The tab lists every reply you owe an answer (the follow-up radar's "you owe a reply", per person, from the day it came in) and what is known about it: - **Logged with the outcome `interested`**: listed as it is, no AI needed. Set the outcome when you log the reply (Settings > Outcomes offers the value to a folder that set its own list before it existed). - **Otherwise, Check replies** asks your AI CLI, with no tools and no web, once per reply: interested, not interested or unclear. The answer is kept in `outreach/replies.json` (one commit) and never changes the interaction. An "interested" from the AI waits for you: **Confirm: interested** or **Not interested**. You can also mark any reply yourself. - **Draft a reply** on an interested reply builds a reply-to-interest draft: no research, their reply is the context. It says the approach's offer line, one proof line, the playbook's **first step offer** (`first_step_offer`, left out when empty: not everyone offers a free first call) and asks one question. No time slot, booking link, link or price: a meeting comes after they answer. The wording template per language is `[outreach.reply_to_interest.]` in `messages.toml`. The card shows their reply above the draft; Regenerate, Skip, Mark ready and the rest work as on a first touch. Once you send and log your answer, the reply leaves the list. ### Follow-ups A message of yours that nobody answered. It is due on the follow-up radar's rule: your latest message to that person has gone `followup_nudge_days` (5, Settings) without an answer and nothing is planned for the company. The bot and the radar read the same setting, so they never disagree. - The **Follow-ups** tab lists who is due (oldest first) with **Draft follow-up**; a batch of them builds from the bar (**Draft type**: follow-up) or with `hermitcrm outreach build --kind follow-up`. The Generate panel on a contact preselects follow-up when one is due. - No research: the thread is the context, and your earlier message goes to the AI as data. The draft is short, has no calendar invite, link, time slot or price, asks one question and gives an easy way out ("a short 'not now' is fine, then I'll close the loop"). The wording template is `[outreach.follow_up.]` in `messages.toml`; `{offer_line}` is an optional line it leaves out by default. - **Who a batch leaves out**: someone whose unanswered message was on another channel than the batch's (or on a call or in a meeting: write that one yourself); someone you already followed up with once (two messages of yours since they last wrote: close the loop); the usual do-not-contact, closed stage and open item. The 30-day cooldown does not apply to a follow-up: it is due after 5. - **When you pick the person** (the Generate panel, **Draft follow-up**, or `--contact`), the first two are a warning on the draft instead of a refusal: switch the channel when they ignore you on one, and draft another message however many went unanswered. The warning is for you; the AI does not see it. A call or a meeting is still left to you. - A cold follow-up email gets the playbook's opt-out line, as a first touch does. A LinkedIn follow-up is a message in the conversation, so it only works if they accepted your connection; the bot cannot know that. ### Add what you see on their LinkedIn profile LinkedIn blocks anonymous reads, so the bot gets little about a person from it. You can add it yourself: on their profile, logged in, click the Hermit CRM bookmarklet ([Extension](https://hermitcrm.io/docs/#extension)). For someone already in the CRM whose card is drafted, it opens **Add to research** with the top card, About and their own recent posts and comments; trim what you like, press the button, and what the AI finds is saved as notes on the contact, sourced to the profile. The page text itself is not stored. ### The steps 1. **Preview**: who a batch would go to, and why not the others. No AI, nothing written. ```text hermitcrm outreach preview --where 'stage=prospect' --channel email --limit 10 ``` 2. **Build**: for each contact in the batch, research, then a draft, then the checks. Each item is a file in `outreach/`; each step is one commit, so `hermitcrm undo` takes any of it back. ```text hermitcrm outreach build --where 'stage=prospect' --channel email --limit 10 ``` 3. **Review**: `hermitcrm outreach list`, then `hermitcrm outreach show ` for the card: the contact and company links, what research found and where, the flags, the culture norms used, the checks, and the draft. Edit the draft in the file if you want: the text below the second `---` is the message. 4. **Ready or skip**: `hermitcrm outreach ready ` approves it. The checks run again on your edited text; one that still fails is a warning, kept on the item, and the choice is yours. The research is already on the contact as notes. `hermitcrm outreach skip --reason R [--note "..."]` drops it, R one of `bad-fit`, `wrong-person`, `bad-timing`, `bad-draft`, `already-in-contact`, `other`. A skipped person comes back in batches after the cooldown; `bad-fit` keeps their company out. 5. **Send and log** it yourself, as you always do (by hand, BCC or the extension). The bot does neither. Logging the message on the item's channel after you marked it ready marks the item **sent**; `hermitcrm outreach sent ` does the same without logging anything. ### Who gets picked - **The filter** is the Companies page's (`stage=prospect`, `country=DE,NL`, `fit_score=>60`, `tags=saas`). A contact key works too (`contact.title=CEO`). Or name people outright: `--contact acme/jane-doe`. The comparison goes inside the value, after the `=`: `fte_estimate=>24`, not `fte_estimate >24`. `>` and `<` compare numbers only on a number field; on a text field they compare letter by letter (100 counts as smaller than 24), and the preview says so. A filter that does not work shows what to type instead and an **Explain and fix with AI** link that opens Ask · Adjust with the problem filled in. - **Order**: highest fit first, by the company field `outreach_fit_field` (default `fit_score`, 0 to 100). A company without one comes last, with "no fit score" on its card; the bot never makes one up. Within a company, someone the playbook has a persona for goes first. - **Left out**, with the reason in the preview: a company in a closed or parked stage; anything tagged `do-not-contact`; a company an approach's disqualifier hits or that misses every approach's ideal customer; a contact the channel cannot reach (email needs an address, LinkedIn a profile URL); a contact with an open item (researching, drafted or ready); a contact you messaged or skipped in the last `outreach_cooldown_days` (30); a company skipped as `bad-fit`; for a first touch, anyone you messaged before. A reply to interest needs an interested reply owed, and skips the cooldown and the fit test: they asked (the approach is the one the conversation started with). - **One item per company**, likely duplicates included: the same website domain, or names that are the same once `-ai`, `-hq`, `GmbH` and the like are dropped. The preview lists such pairs; merge them on the company page. - **Flags, not exclusions**: no country (English and the default culture are used), no headcount, no number of salespeople. The card says what was not checked. - **Which approach**: the one the company fits best (see the playbook). A tie takes the first one in the playbook and says so; `--approach NAME` picks. ### Research Two levels, `outreach_research` in `config.toml` (default `scrape`), or the **Research** select on the Outreach page and `--research` on the command line. **`scrape`** (free): the company website, its LinkedIn page and the person's LinkedIn page are read (LinkedIn usually refuses anonymous requests; the card says so), then one small AI call with no tools lists the facts on them. Every finding has its source; one without is dropped. A finding older than about 90 days is marked stale and never opens a message. Nothing found: the draft uses the approach's parts from the playbook and marks gaps `[like this]`. A website, LinkedIn URL or country found for an empty field is proposed on the card (a **Save to the record** button), never written without it. **`ai-web`** (AI tokens): the scrape first, then one more AI run that may search the web and read pages, for what the company's own pages do not say: recent news, a hire, a launch, something the person said. Only Claude Code can be held to that (web search and fetch only, no files, no shell, no MCP servers, run in an empty temporary folder); with any other AI CLI the level is refused with the reason, and you use `scrape`. What the run is told is public only: the company name, website, country, LinkedIn pages, the person's name and title, and what the scrape found. Never your notes, messages, email addresses or phone numbers, since a page written by a stranger could try to make the run repeat what it knows. A finding stays only if it names a page the run listed as read, that page is an http(s) address, and it can be read again (LinkedIn is taken as listed); the rest are dropped, never shown as fact. If the run fails, the scrape findings stand and the card says why. ### The draft and its checks A first touch opens with something about them (never "I"), says the offer in one line, and ends with exactly one question about their situation: no meeting ask, no link, no time slot. It is written in the contact's draft language, otherwise the company's country's, with the culture norms of `[culture.]` in `messages.toml` (German first contact uses `du`); your playbook's Voice wins over them. The checks are rules, not AI: exactly one question (a LinkedIn connection note may ask none); not opening with I, Ich, Ik or Je; no calendar link or proposed time; the channel's length (email: subject 60 characters, except in a reply or follow-up, which keeps their "Re:" subject however long; 120 words; LinkedIn: a 300-character connection note and a 600-character message for after they accept); only findings and proof lines that exist; no `[ ]` gap. A draft that breaks one is written again once; if it still breaks it, it is shown with the problem marked. Only a fallback draft, written when research found nothing usable, keeps its gaps: it says "needs hand edits". A reply to interest is checked for the offer line (it must be there word for word), one proof line, exactly one question, and no link, time, price or day rate (`€900`, `2 days a week`); on LinkedIn it is a message of up to 600 characters, not a connection note. Drafts use the medium model; `build --strong` uses the strong one. The scrape research and the critic read and score rather than write, so they run on the light model whichever you pick (Claude: Haiku; `enrich_model_light`, see [Ask the Hermit](https://hermitcrm.io/docs/#ask)). The `ai-web` search run uses the model of the draft. **Framework.** A first touch is written with a framework: a recipe for its parts, its length and what it asks (`hermitcrm help playbook`). `build --framework ID` writes the whole batch with one; without it each draft uses its approach's `frameworks: default`, else `house`, today's recipe. An id the library does not have is refused, and so is `--framework` on a reply or a follow-up, which have no framework. A framework can need something the research must find: a premise (something they wrote, said or shared) or a personal connection (a note you wrote). When it was not found, the draft uses the approach's default if that needs nothing, else `house`, and the card says so in its flags, for example "connection-problem-objection needs a personal connection; drafted with house". Regenerate checks this again against your notes as they are now. The item records the `framework` it was drafted with, and `outreach show` prints it. Regenerate uses that framework. A build or a Regenerate checks the AI's answer for its proof line; Mark ready checks the text again for that framework's length and questions, not for proof. An item from before frameworks has none and counts as `house`; one whose framework has since left the library is drafted with `house` and says so in a flag. **Opt-out line** (optional): set `opt_out` in the playbook's front matter, one sentence or one per language (`opt_out: {en: "...", de: "..."}`), and a cold first email gets it added at the end, in the draft's language. It does not count towards the 120 words. Without it, nothing is added. ### The critic The checks cannot tell whether a draft is any good, so after the checks a second AI call (no tools) scores the draft from 1 to 5 on four things: **relevance** (does it touch the likely problem of this role, and your offer), **personal** (is it built on a real finding about them; for a reply, does it answer what they wrote; for a follow-up, does it pick up your earlier message), **tone** (the language, culture norms, channel and your playbook's Voice) and **honesty** (any claim of familiarity or fact with nothing behind it scores 1). The card shows the four scores, the lowest one, and the critic's one-line reason under the checks. `hermitcrm outreach show` prints them too, and `build` adds the lowest score to its line. The lowest of the four counts, so a draft that invents familiarity is caught even when the rest is fine. If it is below `outreach_critic_threshold`, the draft is written once more with the critic's reason as the thing to fix, and **the second draft is kept**, scored again, whatever it scores: never a third time, and a low score never blocks you. That is at most four draft calls and two critic calls per card; `attempts` on the item counts the draft calls. The threshold starts low, **2**: only a draft that scores 1 somewhere is redone, because nothing says yet what a 3 is worth on your own folder. Raise it once you have seen some scores against the drafts you actually liked; `0` turns the critic off. A critic that fails (the CLI errors, the answer is not four scores) leaves the draft as it is, and a flag on the card says "critic did not run". **Re-check** also asks the critic about the text as you edited it (an AI call; it scores only and never rewrites your text). Without an AI it runs the checks and says the critic did not run. **Mark ready** does not ask. A score is never a check: the rules stay in the checks and `clean_answer`. ### About you, about them Next to the critic's scores the card counts the words that are about you (I, my, we, our) and the words that are about them (you, your) in the message and the one after they accept, in the draft's language. English, German, Dutch and French have word lists; any other language shows no count. The opt-out line is left out. When there are more about you than about them, the line turns into a warning. It is a flag, not a check: nothing is blocked or written again, and the offer line is first person by nature. In German, the formal `Sie` and `Ihr` count as about them only in capitals; `Sie` not first in a sentence (it may mean "she" or "they" there), the `Ihr` forms and `Ihnen` anywhere. A word in all capitals, like `US` or `WIR`, never counts. `hermitcrm outreach show` prints the same line. ### The files `outreach/.md`, one per item. The front matter holds `state` (`researching`, `drafted`, `ready`, `sent`, `skipped`; `logged` is for later), the channel, contact, company, the reply it answers (`reply_to`) or the message it follows up (`follows`), approach, findings, check results, the critic's scores (`critic`, `critic_reason`, `critic_model`), flags, timestamps, the AI's original text and how much you changed it. The body is the message, byte for byte. `hermitcrm check` reports a file it cannot read. Settings in `config.toml`: `outreach_research` (`scrape` or `ai-web`), `outreach_cooldown_days`, `outreach_fit_field`, `outreach_high_fit` (65: the preview counts high-fit companies never contacted), `outreach_batch_size` (10), `outreach_critic_threshold` (2; 0 = no critic). Channel rules and culture norms: `[outreach.]` and `[culture.]` in `messages.toml`, over the shipped defaults. Related: [Playbook](https://hermitcrm.io/docs/#playbook), [Messages](https://hermitcrm.io/docs/#messages), [Sending](https://hermitcrm.io/docs/#sending), [CLI](https://hermitcrm.io/docs/#cli) ## Sending Sends the email you approved from the Outreach page, now or spaced over the working day, from a mailbox of its own; LinkedIn is a paced session where you click Send yourself. You approve every message. Until now the outreach bot only wrote and checked: you copied each message into your mail or LinkedIn, and logging it afterwards marked the item sent. That is fine at five a day and breaks at fifty. Sending adds two things, both off until you turn them on, and neither one writes anything you did not approve. ### Set it up (about 30 minutes, once) 1. **A separate mailbox.** Use a mailbox on its own domain (for example `sam@try-example.com`, not your main address). Cold email can hurt the reputation of the domain it is sent from; a separate one keeps your main domain safe. Give the new domain SPF, DKIM and DMARC (a DMARC record with `p=none` is enough at this volume). 2. **An app password.** With Gmail or Google Workspace: turn on 2-Step Verification, then make an app password named "Hermit CRM" (). A Workspace admin can switch app passwords off, and changing the account password revokes them. Only Gmail and Google Workspace are tested; iCloud and Fastmail should work; Microsoft has switched off password sign-in, so Outlook.com and Microsoft 365 do not. 3. **Settings > Email & calendar > Sending.** Pick the provider, the sending address and the app password (kept in the Keychain or `.secrets.toml`, never in `config.toml`), tick **Sending is on**, press **Send a test to myself** (a wrong password shows the server's exact reply), then **Turn on scheduled sending**. One app password covers sending and the reply check. (An empty IMAP server switches the reply check, the Sent check and the bounce scan off: then a send that stopped halfway cannot be looked up in Sent, so use it only if your provider has no IMAP.) 4. **An opt-out line** (optional). If your playbook sets `opt_out:` in `MESSAGING.md`, one sentence or one per language, a cold first touch carries it and a scheduled one is held when it was edited out. Without it nothing is added and nothing is checked. The **Opted out** button and the suppression list work either way. 5. **Energy settings.** The Mac must not sleep on power during the window: launchd runs nothing while it sleeps. `hermitcrm doctor` reads the setting and warns; it never changes it. ### What it does On the Ready tab each email card has **Send now** and **Schedule**, and **Schedule all ready email** shows the proposed times first and asks you to confirm. A queued card says when it goes out ("goes out Tue 20 Oct, 09:47"); the **Scheduled** tab lists them with **Unschedule**, **Send now** and **Move to front**. The strip at the top of the page shows today's count against the cap, the next send and **Pause**, the kill switch: one click, effective from the next tick. A job (`hermitcrm send due`, every 3 minutes) sends **one** queued email per run. Before each send it checks again: - sending is on, not paused, inside the window, under today's cap, and the last send was at least the minimum gap ago; - the text is still exactly what you approved (an edit on the page, or by hand in the file, puts it back to Ready); - the contact has an email address that is not on the suppression list, and the company is still open and not do-not-contact; - **nobody there has written to you** since you approved it: in the CRM, and in the sending mailbox itself (one IMAP search, only when an item is about to go out). If they did, it goes back to Ready with "they wrote to you on 16 Oct"; - the checks still pass, and, when your playbook has an opt-out line, a cold first touch still carries it. Nothing is dropped silently: whatever holds an item puts it back on the Ready tab with the reason. #### The spacing How many a day, and when: | Setting | Default | Meaning | |---|---|---| | `send_window`, `send_days` | `08:30-17:30`, Mon-Fri | when mail may go out, in your time zone | | `send_daily_cap` | 50 | the most per day, after the ramp | | `send_ramp` | `[10, 20, 30, 40]` | the cap for weeks 1, 2, 3... counted from the first send | | `send_min_gap_minutes` | 3 | the least time between two sends | | `send_company_gap_hours` | 24 | two contacts at one company (or one email domain) go out a day apart | | `send_grace_minutes` | 10 | the earliest slot after Schedule, so a mistake can be undone | | `send_overdue_minutes` | 20 | later than this an item is planned again, never sent in a burst | | `send_email_block_countries` | none | countries never emailed (see below) | The gap between two sends is the window divided by the day's cap, times a factor between 0.6 and 1.4 that depends only on the item (so the page and the worker agree), never below the minimum gap: at 50 a day about 11 minutes, at 10 about 54. Follow-ups and replies to interest go first, then first touches in the order they were queued. An item keeps the time the page showed unless it is overdue. A Mac that slept for two hours plans what is overdue again and sends one at a time; it never catches up in a burst. **How much is too much.** 50 a day is the plan, reached from 10 a day in the first week, 20 in the second, 30 and 40 after that. Raise `send_daily_cap` (and the ramp) in steps, only while replies hold up and bounces stay low. More than 150 a day for more than one or two days in a row is strongly discouraged (a mailbox that does it ends up in spam), so after two send-days above 150 the third is held at 150 whatever the setting says. Settings says the same next to the box. Reputation, not the provider's limit (Gmail allows 2,000 a day), is the ceiling, and a brand new domain deserves a slower start than the default ramp: lower `send_ramp` if you are unsure. **Send now** ignores the window and the gap (a reply to interest should not wait for a slot), but it counts toward today's cap, runs the same checks and is refused while sending is paused. It is meant for replies and one-offs. #### When it goes wrong | What happened | What it does | |---|---| | The login is refused (a revoked app password) | Pauses everything; a red banner on every page shows the server's own words; nothing retries until a new **Send a test** passes | | The daily quota is exceeded | Pauses until the next window, then plans again | | A temporary error (421, 450, a timeout, no network) | Tries again in 15 minutes, up to 3 times, then the item is failed | | The address does not exist | The item is failed and the address goes on the suppression list | | The process stopped between sending and logging | The next run looks for the Message-ID in Sent: found, it logs it; not found, it sends again with the same Message-ID. Never twice | | Too many bounced (more than 3 of the last 50, or over 5%) | Pauses with a banner: a list-quality problem, check where the addresses came from | Each message gets a `Message-ID` on your sending domain, written on the item and committed **before** it goes out; a follow-up or reply carries `In-Reply-To` and `References`, so it threads under the mail it follows. The mail is plain text, UTF-8, exactly as approved: no HTML, no links in a first touch, no tracking pixel, no BCC and no List-Unsubscribe header (a one-to-one mail that looks like a newsletter is what filters punish). It is logged once, as an outbound email with `source: outreach`; a later BCC import of the same mail adds nothing. ### Bounces and opt-outs Every half hour, while something is queued or went out in the last three days, the sending mailbox is searched for delivery reports (from mailer-daemon or postmaster). An address that bounced goes on the **suppression list** (`outreach/suppressed.toml`: address, reason, date, item), a note goes on the contact's timeline and the item is flagged. The planner never schedules a suppressed address and the worker never sends to one. **Opted out** (on a reply under Replies to interest, on the contact page, or `hermitcrm outreach opt-out company/contact`) puts the address on the list and skips every open item for that person. Delete a table from the file to mail an address again. ### LinkedIn LinkedIn's User Agreement (8.2) forbids automated sending, and a restricted account is lost for good, so Hermit does not send on LinkedIn, ever. The **LinkedIn session** on the Outreach page puts one approved touch in front of you at a time: 1. **Start session.** Counters at the top: invites today 0/20, the last 7 days 0/80, messages today 0/40, this session 0/10. 2. A card shows the person and the text (a connection note up to 300 characters, a message up to 600). **Open profile & copy** copies the text and opens their profile in a new tab. You click Connect, Add a note, paste, Send. 3. Back on the card: **Sent** logs an outbound LinkedIn message with the exact text; **Already connected** turns the card into the message that would have followed the note; **Couldn't send** asks why (profile gone, LinkedIn's weekly-limit message, something else). The weekly-limit message pauses invites until the 7-day count drops. 4. The next card unlocks after a 45-120 second countdown that varies. After 10 touches the session is done and the next can start two hours later. The queue is answers and follow-ups to people you already talk to, then connection notes, then messages to people who accepted. Sent invites wait under **Waiting to accept**; **Accepted** brings the person's message into the queue. An invite pending for 21 days gets a reminder to withdraw it: many pending invites count against the account. Realistic volume is about 20 invites and 30-40 messages to connections a day. 50-100 invites a day is not available by any method. ### Countries German and Austrian law (UWG §7(2) no. 2, TKG §174) require prior consent for advertising email, business to business included, and an opt-out line does not cure that. The UK and the Netherlands are looser for B2B. This is not legal advice. `send_email_block_countries` (Settings > Sending > Never email these countries) keeps a country's companies out of the queue; the card then suggests LinkedIn. It is empty by default: each folder decides. ### From the command line ``` hermitcrm send test log in over SMTP and IMAP and mail a test to yourself hermitcrm send status on/off, today's count against the cap, the queue, why paused hermitcrm send due [--dry-run] one tick of the worker hermitcrm send pause | resume the kill switch hermitcrm outreach schedule ID... | --all-ready [--dry-run] hermitcrm outreach unschedule ID | front ID | retry ID | send-now ID hermitcrm outreach opt-out COMPANY/CONTACT hermitcrm schedule install --send ``` Agents never schedule or send unless you ask in that conversation: there is no MCP tool for it, and the folder's `AGENTS.md` says so. The real limit on any damage is the daily cap and the window. Related: [Outreach bot](https://hermitcrm.io/docs/#outreach), [Playbook](https://hermitcrm.io/docs/#playbook), [Settings](https://hermitcrm.io/docs/#settings), [CLI](https://hermitcrm.io/docs/#cli) ## Learning What your edits and the answers to your messages teach the outreach bot, as short lessons in `MESSAGING.md` that you accept one by one. Every time you change a draft before approving it, and every time a message gets (or does not get) an answer, Hermit CRM keeps a signal. Nothing is learned silently: a run turns the signals into **proposals**, and a proposal only changes your Lessons when you accept it. ### The phases The more you have reviewed and the more replies you have, the more weight goes to outcomes over edits. `hermitcrm learn report` opens with a banner such as "12 reviewed drafts, 2 replies, 1 positive: phase 1". Pin a phase with `learning_phase_pin` in `config.toml`. ### The steps 1. `hermitcrm learn report` shows the efficiency trend (are your edits getting smaller?), the patterns in your edits, the outcome rates per persona, channel, language and framework (cells under 10 sent items are marked "too few"), and a style table. No AI is used. The framework is the one a first touch was built with (`outreach build --framework`); older items, replies and follow-ups have none and count under "-". A card names its framework too, so a lesson can tell the frameworks apart. 2. `hermitcrm learn status` shows what a run would send: the cards (anonymised evidence from your edits), the prompt size and a token estimate. 3. `hermitcrm learn run` asks the AI once (strong tier) for proposals. With `learning_distill = "handoff"` in `config.toml` it writes `learning/evidence-pack.md` instead; give that to your own agent, save its answer as `learning/proposals.json`, then `hermitcrm learn import`. 4. `hermitcrm learn accept ID [--text "..."]` or `reject ID`. A rejected proposal is never proposed again. 5. `hermitcrm learn retire LESSON_ID` removes a lesson; `hermitcrm learn move LESSON_ID worked|failed|open` moves one. 6. `hermitcrm learn suggest` lists deterministic suggestions (a lower email word limit, phrases you keep deleting); `hermitcrm learn apply ID` applies one. ### Examples instead of rules Lessons are advice. Examples show the drafter what you mean: the closest earlier cases, picked by the same persona, channel and language and by what the findings have in common. Two kinds exist, and both are built from your own history, with names replaced: - **Corrections**: the paragraph the AI wrote and the way you changed it, with your reason. They come from drafts you explained or changed a lot. - **Messages that worked**: the text of a first touch that got a positive reply, a deal or a win. Newer and higher-rung ones count more. Early on corrections lead; once replies and deals exist, the ones that worked do. If nothing is close enough, nothing is shown. This is off until you choose it in Settings (Playbook > Examples in drafts), or set `learning_exemplars` to `corrections`, `outcomes` or `both` in `config.toml`. It adds roughly 300 to 500 tokens to each first-touch draft (`learning_exemplar_count`, default 2, sets how many). A card says which earlier messages a draft was modeled on, and the Examples section of the Learning page lets you pin an example, or exclude one you do not want copied (a message that worked for a one-off reason). Drafts are told to copy the approach, never the words, but similar openers can still creep in: watch for that. `hermitcrm learn exemplars --item ID` shows what the prompt for one item would contain, with the reason for each example and a token count; no AI is used. Add `--approach NAME` when you have more than one approach; the default is the first. Tell Hermit why you changed a draft with `hermitcrm outreach ready ID --tag too-long` (repeatable). Related: [Outreach bot](https://hermitcrm.io/docs/#outreach), [Playbook](https://hermitcrm.io/docs/#playbook), [CLI](https://hermitcrm.io/docs/#cli) ## Reports `/reports`: numbers for a period, each with the change against the previous period of the same length. ### Period `7d`, `30d` (default), `90d`, `quarter`, `ytd` or `custom` with from and to dates. The CLI prints the same report: `hermitcrm report --days 30`, or `--from D --to D`, `--md` for Markdown tables. ### Sections - **Activity**: interactions per ISO week, by channel and direction; companies touched; new companies and contacts. - **Funnel**: entries per stage from `stage_history`, conversion between prospect, engaged, discovery, offer and won (your own open stages, then the first won stage), median days in stage, and the current pipeline with its monthly value. - **Outcomes**: won, lost and disqualified in the period (by the closing date from the stage history, else `stage_changed`), win rate (won stages over won plus lost stages, by role), top 10 lost reasons. - **Messages**: sent, and the results by language, by channel and for the five most reused texts. - **Sources**: companies created and won per source. - **Hygiene**: overdue next steps, silent accounts, contacts without email. Tables with CSS bars, no charts library. Companies without a `stage_history` (created before it existed) can get one from git with `hermitcrm backfill-history --apply`. Related: [Pipeline](https://hermitcrm.io/docs/#pipeline), [Messages](https://hermitcrm.io/docs/#messages), [CLI](https://hermitcrm.io/docs/#cli) ## Make it yours Describe a change to Hermit CRM and your own AI agent builds it in your data folder; this page is the rules that agent follows, and the first thing it reads. For people: open **Make it yours** in the sidebar (`/yours`), or the **Adjust** tab of the **Ask · Adjust** button on any page. Pick an idea or describe a change, then press **Open in Claude Code** (or your agent's button, or Copy prompt). Your agent does the work in your data folder. Hermit picks the change up on the next page you open, without a restart, and lists it under **Recent changes** with an **Undo** button. Hermit never builds the change itself, and never sends a message: routines and templates only write drafts for you to send. ### For the agent: read this first You were asked to change Hermit CRM itself: a field, the look, a dashboard, a page layout, templates, a routine or many records at once. Work in the data folder (the one with `config.toml` and `companies/`), and: 1. Find the request in the table under **Which recipe**, run that `hermitcrm help ` and follow it. If none fits, read `hermitcrm help adjust-feature`. 2. Change only the files listed under **Files you may write**. Records change only through the CLI. 3. Run `hermitcrm check`. Fix every line it prints and run it again until it says `OK`. 4. Make one commit per change, with only the files you changed: `git add fields.toml && git commit -m "ai: adjust: contract renewal field"`. Never `git add -A`: the folder may hold the user's own unfinished edits. 5. Tell the user what changed, where to see it, and how to undo it (the commit id, `hermitcrm undo `, or Undo on the Make it yours page). If the request is unclear or would change many records, ask before writing. A request that quotes a filter problem ("Filter … failed: …", from Explain and fix with AI): explain in plain words what is wrong, give the corrected filter, and change nothing. If the cause is a field type (`>` on a text field), show the `fields.toml` change and wait for a yes before making it. ### Files you may write Without asking, and only these: - `fields.toml`: fields of the user's own (`hermitcrm help adjust-fields`) - `layout.toml`: page layout (`hermitcrm help adjust-layout`) - `dashboards/*.toml`: dashboards (`hermitcrm help adjust-dashboards`) - `routines.toml`: routines (`hermitcrm help adjust-routines`) - `theme.css`: the look (`hermitcrm help adjust-look`) - `messages.toml` and `MESSAGING.md`: draft wording and the playbook (`hermitcrm help adjust-messages`) - in `config.toml`, only the keys `task_types`, `outcomes` and `silent_days`; leave every other line as it is. ### Records only through the CLI - `hermitcrm add company|contact|interaction ...` creates a record. - `hermitcrm set ...` changes records in bulk. It shows a dry run first (how many, a few before and after); show it to the user, and only then run it again with `--apply`. See `hermitcrm help adjust-bulk`. - `hermitcrm stages ...` renames, adds, moves or removes a deal stage. A stage name is stored on every company, so never edit the `stages` line of `config.toml` yourself. Dry run first, show it, wait for a yes. See `hermitcrm help adjust-stages`. - These commit by themselves; do not commit their files again. ### Never - Rewrite an interaction body: it is the record of what was said. - Touch `.hermitcrm-format`, `PIPELINE.md` (generated), `.secrets.toml`, `.claude/settings.json` or anything under `.git/`. - Rewrite history: no `git reset --hard`, `commit --amend`, `rebase` or `push --force`. Undo with a new commit instead. - Send anything: no mail, no LinkedIn message, no API call on the user's behalf. Messages are drafts; the user sends them. - Edit the installed Hermit CRM package (its code, templates or `tokens.css`): an update overwrites it. A wish that needs code is `hermitcrm help adjust-feature`. - Put a password, token or API key in any file in the folder. ### Which recipe | The user asks for | Run | |---|---| | A new field, a field shown in a list or on the cards, task types, outcomes | `hermitcrm help adjust-fields` ([read it](https://hermitcrm.io/docs/#adjust-fields)) | | Colours, fonts, a more compact look | `hermitcrm help adjust-look` ([read it](https://hermitcrm.io/docs/#adjust-look)) | | Draft templates, a language, the playbook | `hermitcrm help adjust-messages` ([read it](https://hermitcrm.io/docs/#adjust-messages)) | | Bringing data in, connecting another tool | `hermitcrm help adjust-connect` ([read it](https://hermitcrm.io/docs/#adjust-connect)) | | Something none of these covers, a new kind of record | `hermitcrm help adjust-feature` ([read it](https://hermitcrm.io/docs/#adjust-feature)) | | A dashboard, a saved view, a ranked list, a pin in the sidebar | `hermitcrm help adjust-dashboards` | | Hiding a field or a section, reordering a page, list columns | `hermitcrm help adjust-layout` | | Tagging, scoring or moving many records at once | `hermitcrm help adjust-bulk` | | Renaming, adding, reordering or removing a deal stage | `hermitcrm help adjust-stages` | | Something that should happen every day or week (drafts, briefs, reminders) | `hermitcrm help adjust-routines` | ### Check and pickup `hermitcrm check` validates the records and every file above. Each problem is one line, `: : `, often with a hint: ```text fields.toml: renewal: type must be one of text, number, date, select (did you mean date?) theme.css: line 3: --acent is not a token the app uses (did you mean --accent?) ``` The running app re-reads `config.toml`, `fields.toml` and `messages.toml` when they change, and its index after any commit, so a change shows on the next page load. `theme.css` is read on every page. The Make it yours page shows the same problems as `check`, so the user can paste them back to you. ### Undo ```bash hermitcrm undo a1b2c3d # a new commit that reverses a1b2c3d (git revert) ``` It refuses an unknown commit, a merge, the folder's first commit, and a folder with uncommitted changes to tracked files. When later changes touched the same lines it stops, leaves the folder as it was, and says so; then undo by hand with a new commit. The Undo buttons under Recent changes do the same. An undo is itself a commit, so undoing it brings the change back. ### The safety badges Each idea on the Make it yours page says how safe it is: - **undo in one click**: one commit to one file; Undo takes it back. - **dry run first**: changes records; your agent shows how many and a few examples before anything is written. - **preview first**: an import; you see every row and what will happen to it. - **starts paused**: a routine; it does nothing until you preview it and turn it on, and it only ever writes drafts. ### How the request reaches your agent Hermit hands the request to the agent named by `adjust_agent` in `config.toml` (Settings > AI): `claude`, `cursor`, `codex`, `gemini` or `copy`. Without it, it follows the tool Enrich uses (`enrich_provider`, or the name `enrich_command` starts with) when that is Claude Code, Codex or Gemini CLI, and otherwise gives you a prompt to copy. - **Claude Code**: a `claude-cli://` link opens a terminal in your data folder with `/hermit Asked on (): ` typed in. `/hermit` is a skill in the folder that runs `hermitcrm help adjust`. Nothing runs until you press Enter. If nothing opens, run `claude` once in a terminal and try again. - **Cursor**: a link puts the request in the Cursor window in front; open the data folder there first. - **Codex, Gemini CLI**: a command to paste in a terminal (`cd && codex '...'`). - Every other agent: **Copy prompt**, which starts with `Run "hermitcrm help adjust" first and follow it.` A link longer than 500 bytes fails without a word on macOS, so a longer request gets Copy prompt instead of Open, with a line saying why. ### Without a shell (Claude Desktop, ChatGPT) An AI app connected over MCP (`hermitcrm mcp`, see [AI agents](https://hermitcrm.io/docs/#ai-agents)) cannot run commands, so it has tools for the same steps, and they keep the rules above for it: | With a shell | Over MCP | |---|---| | `hermitcrm help adjust`, `hermitcrm help adjust-` | `adjust_help` | | read a file you may change | `adjust_read` | | edit it, `hermitcrm check`, commit | `adjust_write`: refused, with the problems, until check passes; then one commit | | the `config.toml` keys | `adjust_config` | | `hermitcrm set` (dry run), then `--apply` | `bulk_preview`, then `bulk_apply` with its `preview_id` | | `hermitcrm undo ` | `undo` | A routine written over MCP arrives paused. A prompt copied from Make it yours says to run `hermitcrm help adjust`; over MCP, call `adjust_help` instead. Related: [AI agents](https://hermitcrm.io/docs/#ai-agents), [CLI](https://hermitcrm.io/docs/#cli), [Settings](https://hermitcrm.io/docs/#settings), [Backups and undo](https://hermitcrm.io/docs/#backups) ## Settings `/settings`: who you are, mail and calendar capture, backup, appearance, fields of your own, AI (Enrich and Ask the Hermit), outcomes, the daily schedule, access from a phone or an AI client, Support Hermit, and About. The page has five tabs: **General** (You, AI, Appearance, Support Hermit, About), **Email & calendar** (BCC capture, Calendar), **Your CRM** (Fields, Drafts, Task types, Deal stages, Outcomes), **Backup**, and **Advanced** (Schedule, access from a phone or an AI app, checking the installation). A section that is set up leads with its status, such as the last import; its form opens under **Change …**, and **How it works** links to its heading on this page. Every section writes `config.toml` in the data folder, keeping its comments; secrets (the mail app password, the calendar URL) go to `.secrets.toml` (mode 600) or the system's secret store, never to `config.toml`. The secret store is the Keychain on macOS and the Secret Service on Linux (GNOME Keyring, KWallet or KeePassXC, reached through `secret-tool` from `libsecret-tools`); Settings offers it only when it answers. Without one, secrets are stored in plain text in `.secrets.toml`, readable only by your user account and never committed to git. Full-disk encryption protects them when the machine is off. A Linux keyring is locked while you are logged out: if the daily import runs then (systemd lingering), keep the app password in `.secrets.toml`. The first time the web app starts without an owner email it opens this page once; "Skip for now" goes to the board. The same questions run in the terminal as `hermitcrm setup`. ### You Your name (its first word signs the drafts) and the addresses you send mail from, comma-separated. Mail from these addresses counts as outbound; their non-freemail domains become `bcc_ignore_domains`, so colleagues are never logged as contacts. ### BCC capture BCC or forward mail to a tracking address (for Gmail, `you+crm@gmail.com` is suggested) and Hermit CRM reads it over IMAP with an app password. Gmail needs 2-step verification and an app password; the page shows the Gmail filter to add so the copies skip your inbox. **Test connection** runs a dry-run import. Per mail: an exact contact email match logs there; a new person at a company whose website or contact domain matches becomes a contact. A domain no company has becomes a new company, named after the domain (`acme-labs.de` becomes "Acme-labs", rename it any time), with the contact and the interaction in the same commit -- but only for mail you sent or forwarded yourself, never for a no-reply sender, and not when a company of that name already exists. Set `bcc_create_companies = false` in `config.toml` to send those to the review queue instead. Everything else (personal addresses, a domain that fits several companies) goes to the review queue, which is **To file** on Home. Dedup is by Message-ID. Below the form: the last import (time and summary, or the error) and **Import now**, which runs the import on the spot. ### Sending Send approved outreach email from the Outreach page, from a mailbox of its own. The provider (Gmail and Google Workspace are tested), the sending address, an app password (Keychain or `.secrets.toml`), the window and days, the daily cap (50; above 150 is held at 150 after two days in a row), the ramp for a new mailbox (10 a day in week 1), the gaps, and countries never emailed. **Send a test to myself** logs in over SMTP and IMAP and shows the server's exact reply on failure; **Turn on scheduled sending** installs the worker job. [How it works](https://hermitcrm.io/docs/#sending). ### Calendar The secret ICS address of your calendar (Google: Settings, your calendar, Integrate calendar, secret address in iCal format; `webcal://` is fine). It is tested with a dry run and never shown again. Past meetings with an external attendee become `meeting` interactions; unmatched ones wait under **To file** on Home; events in the next 7 days show on the Calendar page. Below the form: the last meeting import and **Import meetings now**. ### Backup **On this computer.** Every 5 minutes the folder is backed up to a local repository that can only grow, so a reset, a deleted `.git` or a force-push loses nothing. **Start local backups** makes the first one and schedules the rest (only the backup job; the daily sync stays as it is); `hermitcrm schedule install` does the same and more. The section is *done* once a backup has run. It lives in `~/.hermitcrm/backups/`, outside the folder; set `backup_dir` in `config.toml` to keep it elsewhere, such as another disk. A computer has one backup job: if it already backs up another folder, the button says so and leaves it alone. **Online copy (optional).** A git remote (must be **private**: the folder holds your contacts and mail). Saving sets or updates the remote, tries a push, and turns `push_enabled` on when it worked. Pushes then run in the background after every commit and never block. The remote is the copy off this machine; the local backup is the one nothing can rewrite. See [Backups and undo](https://hermitcrm.io/docs/#backups). ### Appearance Night mode: off (`theme = "light"`), on (`"dark"`) or follow the operating system (`"system"`). Date format: one format for every date the pages show and every date field takes (lists, forms, reports, the calendar). Pick `YYYY-MM-DD` (`date_format = "iso"`, the default), `DD-MM-YYYY` (`"dmy-dash"`), `DD/MM/YYYY` (`"dmy"`) or `MM/DD/YYYY` (`"mdy"`). Date fields are text boxes in that format, with a calendar button; `YYYY-MM-DD` is always accepted as well. Your files, exports and what your AI tool reads stay `YYYY-MM-DD` whatever you pick. #### Your own look: theme.css To change colours or fonts, put a file called `theme.css` in your data folder. The app loads it after its own styles, so whatever it sets wins, and it is committed with the rest of your data. Delete the file to go back to the default look. Settings > Appearance says whether the app is using it. The easiest way is to ask your AI tool, for example "make my Hermit CRM blue" or "put the titles in the same font as the rest". Point it at this page (`hermitcrm help settings`). Most looks need one `:root` block. Each colour is `light-dark(light value, dark value)`, so one line covers both night mode off and on. The colours most worth changing, with their defaults: ```css /* My look for Hermit CRM. Delete this file to go back to the default. */ :root { --accent: light-dark(#166b4a, #6bc49a); /* links, buttons, focus */ --bg: light-dark(#f5f1e8, #1c1b18); /* page background */ --surface: light-dark(#fbf9f4, #24221e); /* cards and tables */ --text: light-dark(#17181a, #ece7dc); /* body text */ --muted: light-dark(#5c574e, #a8a193); /* secondary text */ --line: light-dark(#d2c8b4, #3f3a33); /* borders */ --line-strong: light-dark(#8f8778, #7a7264); /* borders of fields you type in */ --sidebar: light-dark(#efebe2, #211f1c); /* the menu on the left */ } ``` Keep only the lines you change. Fonts are tokens too: `--sans` is the main font (the system's sans-serif), `--title-font` sets the page titles, the name in the menu and the numbers on Home (`var(--serif)`, the system's serif), and `--mono` sets code and text boxes. `--title-font: var(--sans);` puts the titles in the main font; `--sans: ui-serif, Georgia, serif;` sets everything in a serif. Every token and its default is in `static/tokens.css` inside the Hermit CRM package; read it there, do not edit it (an update would overwrite it, and the website shares it). The file can also hold ordinary CSS rules, which win over the app's own. A ready-made cooler look, white and grey with a blue accent and sans-serif titles (Hermit CRM's look until September 2026): ```css /* A cooler look: white, grey and blue. Delete this file to go back to the default. */ :root { --bg: light-dark(#fbfbfc, #16171b); --surface: light-dark(#ffffff, #1e1f24); --surface-2: light-dark(#f7f7f9, #23242a); --subtle: light-dark(#f2f2f5, #2a2b32); --text: light-dark(#1b1b1f, #e6e6ea); --muted: light-dark(#5c5c66, #a4a4ae); --faint: light-dark(#8e8e98, #7c7c86); --line: light-dark(#d9d9de, #34353d); --line-strong: light-dark(#8e8e98, #7c7c86); --accent: light-dark(#1a4fd6, #7ea6ff); --info-bg: light-dark(#eef4ff, #1d2a44); --info-line: light-dark(#c7d8ff, #2f4675); --sidebar: light-dark(#eeeef1, #1b1c21); --sidebar-hover: light-dark(#e2e2e7, #26272e); --sidebar-active: light-dark(#d9d9e0, #30313a); --shadow-color: light-dark(rgba(20, 20, 30, .14), rgba(0, 0, 0, .5)); --title-font: var(--sans); } /* section labels in lowercase instead of small caps */ h2 { font-variant-caps: normal; letter-spacing: 0; text-transform: lowercase; } ``` Two limits keep your data private. The file cannot load anything: `@import` and `url()` pointing at another file or website are blocked by the app, and `hermitcrm doctor` and Settings name the line. Colours and fonts already on your computer work; `data:` URLs work too. Keep text contrast readable, at least 4.5:1 against the background. ### Fields Fields of your own, on top of the ones Hermit CRM has: a key, a label, a type (text, number, date or select) and where they show up. Add them one at a time, or edit `fields.toml` whole in the box below the form -- that box is the file itself, so what you save is what you are reading. Their values live in the front matter of the record they belong to, like every other field. Removing a field stops Hermit CRM showing it and leaves every value where it is; describing it again brings them back. See [the data format](https://hermitcrm.io/docs/#data-format). ### BCC capture, per mail provider Pick your **mail provider** and the IMAP server fills itself in, with a note on where that provider hides its app password. Every one of them wants an app password rather than your normal one, and that is the step people get stuck on. - **Gmail / Google Workspace**: `imap.gmail.com`. Turn on 2-step verification first; the app password page does not appear until you do. - **iCloud Mail**: `imap.mail.me.com`, an app-specific password from your Apple account. - **Fastmail**: `imap.fastmail.com`, an app password with IMAP access. - **Outlook.com / Microsoft 365**: Microsoft has switched off password sign-in for IMAP on most accounts, so this usually fails however the password is made. What works is forwarding or BCC'ing to a Gmail or Fastmail address kept for the purpose, and pointing Hermit CRM at that one. ### AI: Enrich and Ask the Hermit `enrich_provider` (`auto`, `claude`, `codex`, `gemini`, `grok` or `custom`), `enrich_command` and `enrich_timeout`. **Account** (`enrich_account`) says how that CLI is signed in: `subscription` (a ChatGPT, Claude or Gemini plan, the default) or `api` (an API key). It matters because a plan is not entitled to the same model ids as an API key, and asking for one it does not have fails outright rather than quietly choosing something else: a Codex CLI signed in with a ChatGPT account answers `The 'gpt-5-mini' model is not supported`. On a subscription Hermit CRM asks for no particular model where that is known to matter, and the CLI answers on whatever the plan gets. A model you type into the fields below is always used, whatever the account type. **Model** switches between the medium tier (default; Claude: Opus) and the strong tier (Claude: Fable) for Enrich and [Ask the Hermit](https://hermitcrm.io/docs/#ask); `ai_tier` in `config.toml`. Answers and proposals made on the medium tier offer "Retry with" the strong model. The two model fields (`enrich_model`, `enrich_model_strong`) override the provider defaults; a third, `enrich_model_light` (`config.toml` only), is the cheaper model the outreach bot's scrape research and critic use. The section shows which CLI is in use, or why none is. Under launchd or systemd the process starts with a bare PATH, so the CLI must be in `/usr/local/bin`, `/opt/homebrew/bin` or `~/.local/bin`, or the command must be a full path. See [Enrich](https://hermitcrm.io/docs/#enrich). **Make it yours hands requests to** (`adjust_agent`): the agent that builds what you describe on the [Make it yours](https://hermitcrm.io/docs/#adjust) page. Claude Code and Cursor open with one click, Codex and Gemini CLI get a command to paste in a terminal, and *none* gives you the prompt to paste into any agent. Left on *the tool above*, it follows the tool Enrich uses when that is Claude Code, Codex or Gemini CLI. The General tab's first card, **Make it yours with your AI**, opens the same page, and each section of the Your CRM tab has an *or describe it* link that starts it with a matching request. ### Deal stages The steps a deal goes through, in order: the Pipeline columns, the stage filters and the funnel follow this list. Each stage has a name (lowercase letters, digits and hyphens, at most 30 characters) and a **role** that tells Hermit what it means: | Role | Meaning | |---|---| | open | A column on the board. The first one is where new companies start and where a requalified company returns. | | won | A deal won. Counts for the win rate. | | lost | A deal lost. A reason is required. | | closed | Any other end, such as disqualified. | | parked | Out of the pipeline for now, with a date to come back. | A **valued** open stage shows the monthly value in its `PIPELINE.md` heading. Up and Down reorder, Delete removes a stage, and the blank row adds one. Renaming a stage renames it on every company and in every stage history, in a commit together with the new list, so one Undo takes it all back. Deleting a stage that still has companies asks where they go (and for a reason, when that stage is a lost one). A stage can only become a lost stage when every company in it already has a reason. Without any setting the defaults apply: prospect, engaged, discovery (valued), offer (valued), won, lost, disqualified, temp-disqualified. Dashboards and routines that still name an old stage are listed after the save, not rewritten. The same changes are `hermitcrm stages` for an agent (`hermitcrm help adjust-stages`). ### Outcomes The outcome choices for interactions, one per line, in order: the first is what a detected reply counts as, the last what silence past the message window counts as. Also here: `message_window_days` (default 14) and `silent_days` (default 14, the Calendar's silent list and `PIPELINE.md`). Keep in touch on Home reads `keep_in_touch_days` (default 180), set in `config.toml` only. Duplicates and an empty list are refused. The default is `successful`, `interested`, `unsuccessful`: `interested` marks a reply that shows interest (the Outreach page's Replies to interest). A folder that set its own list before `interested` existed is offered it here, placed before the last outcome so replies and silence count as before; nothing changes until you press **Add interested**. ### Schedule Read-only status of the daily job (`hermitcrm sync --apply`: BCC, then calendar) installed by `hermitcrm schedule install [--at HH:MM] [--serve]` through launchd (macOS) or systemd user units (Linux); on Windows the `schtasks` commands are printed for you to run. The install command to copy is on the page. ### Access Read-only: the two other ways into the same folder, both started from a terminal. **From your phone.** The web app binds to `host` in `config.toml`, `127.0.0.1` by default, which only this machine can reach. `hermitcrm serve --host 0.0.0.0` binds every interface and prints the LAN address to open on the phone; setting `host = "0.0.0.0"` in `config.toml` makes it permanent. Hermit CRM has no password, so anyone who can reach the port can read and write the CRM: use a network you trust, or a private one (Tailscale, WireGuard), not public Wi-Fi. **From an AI client (MCP).** `hermitcrm mcp` speaks the Model Context Protocol on stdin and stdout, so a desktop AI client -- Claude Desktop, ChatGPT desktop, Cursor -- can read and write the CRM with no terminal. You do not run it yourself; the page shows the JSON to paste into the client's MCP config, which starts it. Because it is a local process talking over a pipe, the client has to run on this machine: a phone app cannot reach it, and the phone route is the web app above. Ten read tools and seven writes, each write one commit you can undo. [AI agents](https://hermitcrm.io/docs/#ai-agents) has the tool list. ### Support Hermit Hermit CRM is free and open source. The Support Hermit link in Settings and at the bottom of the sidebar opens its [Ko-fi page](https://ko-fi.com/gijsbos) in your browser, where you can leave a tip. It is a plain link: the app loads nothing from Ko-fi and sends it nothing, and nothing in the app changes whether you give or not. ### About Version and the update check, the data folder, its format version, and `hermitcrm doctor`, which checks the whole installation line by line. The update check asks once a day, sends no identifiers, and says what it found: a newer version, "the latest", "no release published yet" when nothing is published where it asks, or "could not reach ..." when it could not ask at all. Those last two are not the same as being up to date, and it will not say they are. It asks PyPI unless `update_url` in `config.toml` points somewhere else; any URL answering `{"version": "0.4.0"}` works, so a static file on a download page is enough. Turn it off with `update_check = false`. When a newer version exists, the About row and the sidebar show an **Upgrade** button. One click installs it (with `uv tool install hermitcrm@latest`, `pipx install --force hermitcrm` or `pip install --upgrade hermitcrm`, whichever installed Hermit CRM, also from a download), checks that the new files start, restarts the app and asks you to **Reload**. It takes about a minute. Your data is not touched; if the new version changes the data format, the folder is migrated when the app starts, in one git commit (see [Data format](https://hermitcrm.io/docs/#data-format)). If the install fails, or the new version does not start, nothing is restarted and the panel says what happened, with the command that goes back to the version you had. The button is there only when it can work. Hermit CRM has no password, so an app that listens on the network (`host` in `config.toml`) shows the command to type instead, and so does one on Windows, one whose `update_url` is not PyPI, and one started some other way than `hermitcrm serve`. Run the command, then restart the app: `launchctl kickstart -k gui/$(id -u)/io.hermitcrm.serve` on a Mac, `systemctl --user restart hermitcrm-serve` on Linux. **Restart needed.** If Hermit CRM was upgraded behind the app's back (a terminal, an assistant) the app is still running the old version while the files are the new one, and pages can fail in odd ways. The sidebar and About then say **Restart needed** with a **Restart now** button, and an error page in that state says the same. Related: [Interactions](https://hermitcrm.io/docs/#interactions), [Enrich](https://hermitcrm.io/docs/#enrich), [Sending](https://hermitcrm.io/docs/#sending), [CLI](https://hermitcrm.io/docs/#cli), [Data format](https://hermitcrm.io/docs/#data-format) ## Import and export `/import` (and `hermitcrm import FILE`): companies or contacts from a spreadsheet, previewed before anything is written. Export, below, takes everything out again. ### Input Paste a tab-separated table (copied from a spreadsheet) or upload a `.tsv`, `.csv` or `.xlsx` file (first worksheet; dates stay as serial numbers). The first row is the header. ### Modes - **companies**: needs a `name` column. Other recognised columns fill the company fields; contact columns prefixed `founder_` or `contact_` (name, title, email, LinkedIn) create a contact. - **contacts**: needs a person name column; the company comes from a company column or from a non-freemail email domain. Companies are matched by slug or website domain, contacts by email, else by name. The mode is detected from the header (contacts when there is a person-name or email column plus a company column and no company-only column) and can be forced. Header aliases from HubSpot, Apollo, LinkedIn, Pipedrive and Attio exports are recognised; the full list is on the page. Unknown columns are kept as `column: value` lines in one company note, under an `Imported fields ():` line. For a company row, the notes text and that block become one company note, and a re-import adds only lines that no company note or old notes text has yet. ### Preview and apply The preview lists every row with its planned action (**create**, **update** of empty fields only, **keep**, **skip**) and warnings such as an unmapped country or a non-numeric score. Every column can be remapped to a field, to `notes` or to `ignore`. Applying writes everything in one commit: `import: companies created, updated, contacts created`. In the CLI: `hermitcrm import FILE [--mode companies|contacts] [--map "Header=field" ...]` prints the plan; `--apply` writes it. ### Export Settings > Data > Export everything (or `hermitcrm export`) gives you a zip of five CSV files: `companies.csv`, `contacts.csv`, `interactions.csv`, `tasks.csv` and `stage_history.csv`. Exporting only reads: nothing in your folder changes and nothing is committed. - **Column names.** `generic` uses Hermit CRM's own names; `hubspot` and `pipedrive` rename the company and contact columns to what those CRMs call them. Your own fields and any key the app does not know come after the known columns, so nothing is left out. - **For Excel** (`--excel`) starts each file with a UTF-8 byte-order mark, so Excel shows accents correctly. - **Formulas.** A cell that starts with `=`, `+`, `-` or `@` gets an apostrophe in front, so a spreadsheet does not run it. Importing the file back removes the apostrophe again. - **JSON.** `hermitcrm export --format json` writes one file with every company and its contacts, interactions, tasks and stage history. The CLI writes `hermitcrm-export-DATE.zip` in the current folder, or where `--output` says (`-` for stdout). It refuses a path inside the data folder, where the export would end up in git. Every column of the export is one the importer recognises: import `companies.csv` first, then `contacts.csv`. Related: [Companies](https://hermitcrm.io/docs/#companies), [Contacts](https://hermitcrm.io/docs/#contacts), [CLI](https://hermitcrm.io/docs/#cli) ## Extension `/extension`: turn the page you are looking at into a company record, in one click, without typing the name twice. The competition for a CRM is not another CRM, it is the record never getting written. The extension is the short path: you are on a company's website or its LinkedIn company page, you click one bookmark, and a new-company form opens with the fields already filled in. ### A profile makes a contact A LinkedIn or Xing profile is a person, not a company, so it opens the new-contact form rather than the new-company one, with the employer typed into the company box. That box matches what you have already, so capturing three people from the same company puts all three on one record instead of making three companies. **On a LinkedIn profile you are logged in to, the bookmarklet reads the page you are looking at.** Your tab already shows everything, so it sends pieces of that page to Hermit CRM and nothing is fetched: no second request to LinkedIn, no cookies copied, nothing an automation check could notice. It fills in: - **name**, and **title**: the current role from Experience ("Head of Compliance"), or the headline when Experience is not on the page - **company**: the one the page labels as the current company, and its **LinkedIn page**, which a new company keeps -- so the next colleague you capture lands on the same record, even if the names differ slightly - **email**, only when you have opened *Contact info* (your 1st-degree connections) before clicking - the **location**, shown above the form It reads the order of the lines and the labels a screen reader gets, not LinkedIn's class names, which change all the time. When LinkedIn moves things around, a field it cannot place stays empty rather than wrong. If the form comes up nearly empty, the profile had probably not finished loading: click again once it has. Logged out -- a pasted URL, or a bookmarklet taken before this version -- Hermit CRM fetches the profile itself, and LinkedIn shows an ordinary member almost nothing that way: often HTTP 999, otherwise job titles masked as `*****`. The form still opens, with the profile address and the name spelled in it; take the bookmarklet again from this page to read the real thing. A person's headline is never read as a company's one-liner -- that produced sentences like "Most companies don't have a lead problem... - Experience: ... - 500+ connections on LinkedIn", which is how the profile path was found. ### Add to research: someone you have, with a card waiting When the profile is already in the CRM and its contact has an outreach card that is **drafted**, the bookmarklet does not just take you to the contact. It opens **Add to research**: the top card, the **About** section and the text of the **posts and comments by them** from their Activity (their likes, reposts and other people's posts are left out), each in a box you can trim. Press the button and the AI (no tools; it reads the text as data) turns what is left into findings sourced to that profile. They join the card's findings (duplicates are skipped), you can remove them like any other, and **Regenerate** uses them. This is the reliable way to get person-level detail, because LinkedIn blocks anonymous fetches. The text you trimmed is not kept, only the findings; the card's message is not touched. It needs your AI CLI. A card that is still researching or already ready says it cannot take research now. Take the bookmarklet again from this page: older ones do not send About or Activity (the top card still works). ### The bookmarklet The `/extension` page has a link to drag to your bookmarks bar. It points at the address this Hermit CRM is serving on, so take it again if you later run `serve` on a different host or port. Some browsers refuse to run a bookmarklet clicked from a page; dragging it to the bar first always works, and the code is printed on the page to paste into a bookmark by hand. There is also a plain box on the same page: paste a URL, press **Read the page**. Same result, no bookmark. ### What it reads On a LinkedIn profile, the page in your own tab (above). Anywhere else, the page's own metadata, fetched by your machine directly: nothing is sent anywhere, and no AI is involved. It proposes the company name, the website, the LinkedIn company page the site links to, the country, an employee-count hint and a one-line description. The same parser as **Fetch from URL** on an existing company ([Enrich](https://hermitcrm.io/docs/#enrich)); the extension only runs it the other way round, page first and record after. The guesses are guesses. Country in particular is inferred from the page and is sometimes wrong, which is why the form says to check the fields. **Nothing is written until you press Create company.** ### If you already have it It looks for an existing company with that website or LinkedIn page before offering the form. When it finds one it says so and links to it, so a second click on the same bookmark opens the record rather than making a duplicate. A profile you have already saved opens that contact, whichever page of the profile you clicked on: `/in/x/overlay/contact-info/`, `nl.linkedin.com/in/x` and `/in/x?miniProfileUrn=...` are one person, stored as `https://www.linkedin.com/in/x`. Nothing is fetched to find that out. Related: [Enrich](https://hermitcrm.io/docs/#enrich), [Companies](https://hermitcrm.io/docs/#companies), [Import](https://hermitcrm.io/docs/#import) ## Enrich Two ways to fill a company's or contact's empty fields: **Fetch from URL** reads a web page (no AI); **Enrich** asks an AI CLI you already have, on the medium model by default; the proposal page offers **Retry with** the strong model (Claude: Fable). Both show a proposal page first: each proposed value with a checkbox and an editable input, plus the sources. Only ticked fields are written, in one commit (`ai: company enriched`, `ai: contact / enriched`). Fields that already have a value are never touched. ### Fetch from URL On the company page. Reads the given website or LinkedIn company URL (defaults to the company's own) with the standard library: title, meta description, links, JSON-LD and the domain's country TLD become proposals for website, LinkedIn, country and the one-liner. LinkedIn serves its public page to some anonymous requests and refuses others (HTTP 999); the error says so. CLI: `hermitcrm fetch [--url URL] [--apply]`. ### Enrich On company and contact pages when a CLI is available. Company fields: website, LinkedIn, country, one-liner, plus any field of your own that set `enrich = true`. Contact fields: title, LinkedIn. The CLI is asked for a JSON answer with sources; anything it cannot verify comes back as "not found", never guessed. When nothing could be verified but the CLI found sources (say, a LinkedIn profile at another company, which often means the contact sits under the wrong company), the proposal page still opens with its notes and sources to check by hand. A call can take a minute and uses that CLI's credits. CLI: `hermitcrm enrich [--contact ] [--apply]`. ### Which CLI Set on the Settings page or in `config.toml`: `enrich_provider` is `auto` (the first of claude, codex, gemini, grok found), one of those names, or `custom` with `enrich_command`; `enrich_model` and `enrich_timeout` (seconds, default 180) are optional. The binary is looked up on PATH and, because launchd and systemd start with a bare PATH, also in `/usr/local/bin`, `/opt/homebrew/bin`, `~/.local/bin` and the usual npm, bun, pipx and cargo folders. The Settings page shows what was resolved; `hermitcrm doctor` reports it too. Related: [Settings](https://hermitcrm.io/docs/#settings), [Companies](https://hermitcrm.io/docs/#companies), [Contacts](https://hermitcrm.io/docs/#contacts), [CLI](https://hermitcrm.io/docs/#cli) ## Ask the Hermit The **Ask the Hermit** button, top right on every page, sends a question to the AI CLI configured under [Settings](https://hermitcrm.io/docs/#settings) (the same one Enrich uses). ### How it answers 1. **One quick call over a CRM index.** Hermit builds a compact table of your CRM itself: one row per contact with a ready-made link, the company stage and its role (open, won, lost, closed, parked), the last touch, the last thing logged (notes included), the company-level touch, the next open task (marked `overdue` when it is) and the days since the last touch. The model gets that table plus the text of the page you were on (with its links) and answers without tools, usually in 5 to 10 seconds. Questions such as "who did I write to recently and have no follow-up planned?" are answered here. 2. **The data folder if the index is not enough.** For what only the text holds (what a note or an email says), the model says so and a second run starts inside the data folder with read-only tools: reading files and `hermitcrm show`, `digest`, `report` and `check`. The folder's `CLAUDE.md` tells it to read `PIPELINE.md` first and never all interactions. Nothing is ever written. Links in an answer are checked before you see them: a link to a person or company opens their page, a link that names a file or a slug is repaired, and a link that points nowhere is shown as plain text. The answer page says which of the two answered, which model ran, how many seconds it took and, for the data-folder step, the sources it used. ### Buttons on the answer - **Retry with Fable** (or the strong model of your provider): the same question on the strong tier. Shown when the answer came from the medium tier. - **Search every file instead**: skip the index and read the data folder, for when the quick answer missed something. - **Skip the page, search the whole CRM**: for a new question, leave the page out and answer from the CRM index alone. ### Saved questions For the questions you ask again and again. Type one and press **Save** next to **Ask** (in the panel or on the Ask page), or press **Save this question** under an answer; the answer stays where it is. - **While you type**, the saved questions that contain every word you typed are listed under the box. Up and Down choose, Enter or Tab fills the box, Esc closes the list. In an empty box, Down lists them all. - **The four you ask most** are buttons in the panel (see *Recent and most used* below); one click puts the question in the box. **All saved** opens the full list on the Ask page, where you can **Use**, **Edit** or **Delete** each one. - **Whole CRM.** A question saved with *Skip the page, search the whole CRM* ticked keeps that: choosing it ticks the box again, and the panel says so ("Skips this page...") with a link to go back to the page. - **[Placeholders].** A part in square brackets, such as `What did I promise [company] in my last email?`, is selected when you choose the question, so typing replaces it. They are kept in `ask-questions.toml` in your data folder, newest first, one commit per change (`ask: saved question "..."`), so they are in your backups and on every machine you sync. You can edit the file by hand. If it has a mistake Hermit does not touch it: the Ask page shows what is wrong and a save is refused until it is fixed. Saving the same question again (case and spacing ignored) changes nothing. At most 100 are kept. ### Recent and most used - **Most used first.** Every time you ask a saved question, Hermit counts it. The panel's four buttons, the suggestions under the box and the list on the Ask page put the most asked first (the Ask page says "asked 3×"); questions asked equally often stay newest first. - **Recent.** Every question you ask is remembered, 20 at most, newest first, even one that failed (a question that took minutes is not lost). While you type, the ones that match and are not saved are listed under the saved ones in a **Recent** group. Click one to put it in the box, or its **Save** to keep it as a saved question without leaving the page. Recent questions are never buttons in the panel. - **Kept only on this computer.** The counts and the recent list live in `.git/hermitcrm-ask.json` inside your data folder, like the scheduled-send state: never committed, never in `git status`, and not on your other machines. Delete the file and the counts and the recent list start again; nothing else is affected. The saved questions themselves stay in `ask-questions.toml`, in the order they were saved. ### Models Two tiers per provider. Medium is the default; switch it under Settings → AI. | provider | medium | strong | |---|---|---| | claude | `claude-opus-5` (Opus) | `claude-fable-5-1` (Fable) | | codex | `gpt-5-mini` | `gpt-5` | | gemini | `gemini-2.5-flash` | `gemini-2.5-pro` | | grok | `grok-4-fast` | `grok-4` | Override either with `enrich_model` / `enrich_model_strong` in `config.toml` (or the Settings fields) when your CLI names a model differently. Only the claude IDs are verified. Fable needs Claude Code 2.1.251 or newer. Ask the Hermit has its own default: for claude it answers on Sonnet (`claude-sonnet-5-5`), which is plenty for reading a page or the CRM and cheaper than Opus. The other providers' medium models are already their mid-size ones, so Ask uses those. If you set `enrich_model` yourself, Ask follows it; "Retry" still uses the strong model. A third, **light** tier is used only by the outreach bot's scrape research and critic, which read and score rather than write. For claude it is `claude-haiku-5-5` (Haiku). If you set `enrich_model` yourself, light follows it, so Hermit never swaps a model you named; set `enrich_model_light` in `config.toml` to choose one. For the other providers it is the medium model. ### Notes - The page waits for the answer (no JavaScript): usually 5 to 10 seconds for an index answer, up to `ask_timeout` (default 300 seconds) for the data folder. - Only local pages are sent, and only as text; the CSRF token and hidden fields are left out. Related: [Enrich](https://hermitcrm.io/docs/#enrich), [Settings](https://hermitcrm.io/docs/#settings), [AI agents](https://hermitcrm.io/docs/#ai-agents) ## Merge Combine two companies, or two contacts of one company, into one record in a single commit. ### Companies On the company page, pick the company to merge **into this one** and click Compare. The merge page shows every field side by side with a radio per side; the default is this company's value, or the other's when this one is empty. Tags can be combined. A notes row appears only while a company still has an old notes text (from before format 19); company notes are logged interactions and move over with the rest. Contacts and interactions of the dropped company move over (a contact slug that collides gets `-2`), its stage history is merged by date, its folder is deleted, and the commit is `company: merged into `. The history stays in git. ### Contacts On a contact page (when the company has more than one contact), pick another contact to merge into this one. Same side-by-side choice; the dropped contact's interactions are re-pointed and renamed to the kept contact. Commit: `contact: / merged into `. ### Undo Every merge is one commit: `git revert ` in the data folder, then `hermitcrm rebuild`. Related: [Companies](https://hermitcrm.io/docs/#companies), [Contacts](https://hermitcrm.io/docs/#contacts), [Data format](https://hermitcrm.io/docs/#data-format) ## CLI `hermitcrm [--data DIR] `: the data folder is `--data`, then `$HERMITCRM_DATA`, then the current directory. Commands that write take `--apply`; without it they print what they would do. Every write is one git commit. Output is plain text meant to be read or pasted into an AI session. ### Set up and run ```text hermitcrm init DIR [--demo] [--no-setup] new data folder (git repo, config, agent rules, /hermit skill); asks the setup questions hermitcrm sample add|remove the made-up sample account in this folder, with a dashboard and a paused routine: load it, or delete it again (what you changed stays) hermitcrm setup the setup questions again (you, BCC, backup, calendar) hermitcrm serve [--port N] the web app on 127.0.0.1 (port from config.toml, default 8765) hermitcrm doctor [--online] one ok/warn/fail line per check; exit 1 on a failure hermitcrm schedule install [--at HH:MM] [--serve] [--send] [--backup-every MIN | --no-backup] daily sync --apply and a backup every 5 minutes, via launchd or systemd (schtasks printed on Windows); --send also installs the worker that sends scheduled email every 3 minutes hermitcrm schedule remove [--send] remove the jobs (--send: only the sending worker) hermitcrm schedule status hermitcrm migrate [--dry-run] upgrade the data format (every command does this automatically); also puts back a missing /hermit skill (.claude/skills/hermit/) hermitcrm help [TOPIC] these pages; no topic prints the index and the topic list ``` ### Read ```text hermitcrm show SLUG [--bodies N | --all] one company: fields, an old notes text while one is left, contacts, interactions, the last 3 bodies hermitcrm digest [--days 7] recent interactions oldest first, 150 characters of body each hermitcrm followups [--reply-after N] [--nudge-after N] threads you owe a reply, then ones you are waiting on hermitcrm keep-in-touch [--after N] people outside an open deal you have not spoken to in N days (default keep_in_touch_days, 180), longest first hermitcrm brief [--days 7] each upcoming meeting with its stage, next step, contacts and last 3 interactions hermitcrm mcp serve this folder to AI clients over MCP (stdio); see the AI agents topic hermitcrm report [--days N | --from D --to D] [--md] the Reports page as text tables (Markdown with --md) hermitcrm check validate every file (records, fields.toml, layout.toml, dashboards/, routines.toml, theme.css, messages.toml, config.toml, the front matter of MESSAGING.md); exit 1 and one "file: where: what" line per problem hermitcrm playbook [status] per approach: what the playbook has and what the outreach bot still needs (exit 1 while no approach is ready); see `hermitcrm help playbook` hermitcrm playbook upgrade [--dry-run] add the front matter and missing headings to MESSAGING.md, keeping every line; one commit, and a second run changes nothing hermitcrm outreach preview [--where K=V ...] [--contact CO/PERSON] [--channel email|linkedin] [--limit N] [--approach NAME] [--kind first-touch|reply-to-interest|follow-up] who an outreach batch would go to, and why not the rest; no AI, nothing written hermitcrm outreach build (same flags) [--research scrape] [--strong] [--framework ID] research, draft and check each contact: one item per contact in outreach/, one commit per step; refused until the playbook is ready; --framework writes the first touches with that framework (default: the approach's, else house); see `hermitcrm help outreach` hermitcrm outreach list [--state S] every item, newest first hermitcrm outreach show ID one item in full: links, findings, flags, checks, the draft hermitcrm outreach edits [--positive] [--noted] your tracked edits as Markdown: the AI's original, each change with your explainer, and how the message was answered hermitcrm outreach ready ID [--tag T ...] approve a draft (the checks run again on your text; a failing one is a warning); never sent; --tag says why you changed it (too-long, wrong-tone, wrong-pain, bad-finding, wrong-fact, cliche, other) hermitcrm outreach sent ID you sent it (logging the message does this too); logs nothing hermitcrm outreach skip ID --reason R [--note TEXT] drop an item: bad-fit, wrong-person, bad-timing, bad-draft, already-in-contact or other hermitcrm outreach schedule ID... | --all-ready [--dry-run] queue approved email to go out over the working day, spaced and capped; --dry-run shows the times and writes nothing hermitcrm outreach unschedule ID take a queued item out of the queue (back to ready) hermitcrm outreach front ID make a queued item the next to go out hermitcrm outreach retry ID a failed item back to ready hermitcrm outreach send-now ID send one approved email now (same checks, counts toward the cap) hermitcrm outreach opt-out COMPANY/CONTACT they asked not to be written to: suppress the address and skip their open items hermitcrm send test [--to ADDRESS] log in over SMTP and IMAP and mail a test to yourself hermitcrm send status on/off, today's count against the cap, the queue, why paused hermitcrm send due [--dry-run] one tick of the worker (at most one email per run) hermitcrm send pause | resume the kill switch, and lifting any pause ``` See [Sending](https://hermitcrm.io/docs/#sending) for how it works. ### Learning ```text hermitcrm learn report [--approach A] phase, efficiency trend, patterns, outcome rates, style features; no AI hermitcrm learn status [--approach A] what a run would send and cost, pending proposals; no AI hermitcrm learn run [--approach A] distill your edits into proposals (one AI call), or write the evidence pack when learning_distill = "handoff" hermitcrm learn import [--approach A] read learning/proposals.json after a handoff run hermitcrm learn accept ID [--text T] apply a pending proposal to the Lessons (optionally reworded) hermitcrm learn reject ID reject it for good hermitcrm learn retire LESSON [--approach A] remove a lesson hermitcrm learn move LESSON worked|failed|open [--approach A] move a lesson hermitcrm learn exemplars [--approach A] [--item ID] the examples a draft can be shown; --item previews one prompt block hermitcrm learn pin|exclude|unmark ID pin an example, never show it (the way to delete one), or clear that hermitcrm learn suggest list the word-limit and never-write suggestions hermitcrm learn apply SUGGESTION apply one; see `hermitcrm help learning` ``` ### Backups ```text hermitcrm backup [run] [--quiet] back up now: new commits, uncommitted edits, a rewrite kept aside hermitcrm backup status where the backup is, its size, the last run; exit 1 on a warning hermitcrm backup list [PATH] [-n 20] versions in the backup, newest first, optionally only those touching PATH hermitcrm backup restore ID [PATH ...] [--apply] put files back as they were in version ID, as a new commit hermitcrm backup guard block history-rewriting git commands for Claude Code (.claude/settings.json) ``` See [Backups and undo](https://hermitcrm.io/docs/#backups). ### Write ```text hermitcrm add company NAME [--country NL] [--website URL] [--stage S] [--set FIELD=VALUE ...] create a company; prints its slug. Writes and commits at once (no --apply) hermitcrm add contact SLUG NAME [--title T] [--email E] [--phone P] [--role R] [--notes N] create a contact under a company; the name is split into first and last; --notes is logged as a note interaction on them hermitcrm add interaction SLUG --channel email|linkedin|call|whatsapp|meeting|note --direction in|out (not for a note) [--contact CSLUG] [--subject S] [--date D] [--body TEXT|-] log an interaction; --body - reads it from stdin, and a prospect becomes engaged (the first open stage moves to the next) hermitcrm set companies|contacts|interactions --where KEY=VALUE ... [--all] [--set FIELD=VALUE ...] [--unset FIELD ...] [--add-tag T ...] [--remove-tag T ...] [--stage S] [--apply] [--message TEXT] change many records at once. A dry run unless --apply (count, five before -> after samples), then ONE commit `bulk: ` and its undo command. --where is the list pages' filter syntax; no --where needs --all. Bodies and names are never touched. See Make it yours: bulk changes hermitcrm stages [rename OLD NEW | add NAME | move NAME | set NAME | remove NAME] [--apply] the deal stages and their roles; a dry run unless --apply (rewrites companies, one `ai: adjust:` commit per step). See Make it yours: deal stages hermitcrm rebuild rebuild the index and PIPELINE.md, commit "pipeline: rebuild" hermitcrm import FILE [--mode companies|contacts] [--map HEADER=FIELD ...] [--apply] hermitcrm export [--format csv|json] [--profile generic|hubspot|pipedrive] [--excel] [-o PATH] everything as a zip of CSVs (or one JSON file); reads only. See Import and export hermitcrm fetch SLUG [--url URL] [--apply] fields from the website or LinkedIn page (no AI) hermitcrm enrich SLUG [--contact CSLUG] [--apply] fields from an AI CLI hermitcrm bcc [--apply] [--eml FILE ...] import BCC'd or forwarded mail (or .eml files) hermitcrm calendar [--apply] [--ics FILE ...] import past meetings from the ICS feed (or .ics files) hermitcrm sync [--apply] bcc, then calendar, then every routine that is on; what the daily job runs hermitcrm backfill-history [--apply] stage_history from git for companies without one hermitcrm undo COMMIT reverse one commit with a new one (git revert); refuses a merge, an unknown commit or uncommitted changes, and stops on a conflict ``` `bcc`, `calendar`, `sync`, `set` and `stages` with `--apply` tell a running web app to reload its index afterwards. `set` exits 0 for a dry run, a finished change and "nothing matches"; 2 when it refused the request before writing anything; 1 when a write was tried and rolled back (every file is as it was, nothing committed). ### Routines ```text hermitcrm routines [list] every routine in routines.toml, on or paused, and its last run hermitcrm routines preview NAME [--try] who it would pick now, no AI and no writes; --try shows one AI draft, not saved hermitcrm routines run [NAME] [--apply] one routine (even a paused one) or all that are on; one commit per routine hermitcrm routines on NAME turn it on: it runs after each daily sync (commit "routine: NAME: turned on") hermitcrm routines off NAME pause it ``` Drafts go to Home, never out. See [Routines](https://hermitcrm.io/docs/#adjust-routines). `undo` is what the Undo buttons on the Make it yours page run; see [Make it yours](https://hermitcrm.io/docs/#adjust). Related: [Settings](https://hermitcrm.io/docs/#settings), [AI agents](https://hermitcrm.io/docs/#ai-agents), [Data format](https://hermitcrm.io/docs/#data-format), [Make it yours: bulk changes](https://hermitcrm.io/docs/#adjust-bulk), [Make it yours: deal stages](https://hermitcrm.io/docs/#adjust-stages) ## Backups and undo A second copy of the folder's history, outside the folder, that can only grow; every few minutes, and any version can be put back. Claude Code is blocked from the git commands that destroy history. Every change in Hermit CRM is already a git commit, so a bad edit is undone from the history. What the history cannot protect is *itself*: a `git reset --hard`, an amended or rebased commit, a deleted `.git` folder or a force-push loses it. Anything with a shell in the folder can do that -- you, a script, or an AI agent that meant well. The backup covers that case. ### Dangerous git commands are blocked Every data folder has a `.claude/settings.json` that stops Claude Code from running the commands that destroy history, in every permission mode (bypass included). An agent that tries one gets a refusal instead of a result: | Blocked | Why | |---|---| | `git reset --hard`, `git commit --amend`, `git rebase` | move `main` back or replace commits | | `git push --force` / `-f` / `--force-with-lease` | overwrite the copy on your remote | | `git filter-branch`, `git filter-repo` | rewrite every commit | | `git update-ref -d`, `git reflog expire`, `git gc --prune` | delete refs, or the objects a reset left behind | | `git clean -f` / `-fd` / `-fdx` | delete files git does not track | | the same with `git -C ...` | `git -C` would slip past a plain prefix match | | `rm -rf .git`, `rm -rf companies`, `rm -rf ~/.hermitcrm` | delete the history, the data or the backup | | editing `~/.hermitcrm/**` or `.claude/settings.json` | the backup, and this list itself | New folders get it from `hermitcrm init`; existing folders get it with the upgrade (data format 6), merged into any settings file already there. Your own rules in that file are kept. `hermitcrm doctor` warns when a rule is missing and `hermitcrm backup guard` puts them back. Two limits, stated plainly: - **It is Claude Code only.** Codex, Gemini and other agents have no such file; they get the same rules as text in `AGENTS.md`. - **It matches command patterns; it is a seat belt, not a lock.** A script, `bash -c "..."` or a command spelled differently still gets through. That is what the backup below is for: whatever gets through, the versions it destroyed are still in the backup. You are not blocked: these rules apply to Claude Code, not to your own terminal. To let an agent run one of them anyway, remove that line from the file for the moment (`doctor` will remind you to put it back). ### What it is A bare git repository at `~/.hermitcrm/backups/-.git` (or `backup_dir` in `config.toml`; it has to be outside the data folder). It is set up so that nothing in it is ever lost: - a push can add commits and refs, but never move a ref backwards or delete one (`receive.denyNonFastForwards`, `receive.denyDeletes`); - old versions are never cleaned up (reflogs and unreachable objects never expire); - Hermit CRM puts those settings back on every run, should anything change them. ### What each run does To start it, click **Start local backups** in Settings, Backup, or run `hermitcrm schedule install`. `hermitcrm backup` (the scheduled job runs it every 5 minutes): 1. **Uncommitted edits.** If a file differs from the last commit -- a hand edit, an agent that forgot to commit -- that state is saved as `refs/snapshots/