Credential-isolated approval layer between AI agents and external actions, with bounded multi-account Gmail/Outlook triage and Telegram-approved send, Trash, and unsubscribe operations.
Agents propose. Humans approve. Scripts execute. No AI in the execution path.
AI agents with email and API access are a prompt injection timebomb. Current mitigations:
- "Trust the system prompt" β behavioral, not structural. Fails under injection.
- "Restrict the agent's tools" β if it can draft AND confirm, it can still send anything.
- "Remove access entirely" β then what's the point of having an agent?
When deployed with the isolation requirements below, Nightdrop enforces a structural control plane that prompt injection alone cannot bypass.
βββββββββββββββ ββββββββββββββββ ββββββββββββββββ ββββββββββββββββ
β AI Agent ββββββΆβ Draft Queue ββββββΆβ Telegram Bot ββββββΆβ Executor β
β (any stack) β β (JSON files) β β (approval UX) β β (providers) β
βββββββββββββββ ββββββββββββββββ ββββββββββββββββ ββββββββββββββββ
writes watches human reviews sends/executes
draft files for new files approve / deny approved drafts
- Your agent writes a JSON draft file to an inbox directory.
- Nightdrop picks it up and sends a preview to your Telegram.
- You tap Approve or Deny.
- If approved, a deterministic script executes exactly what you reviewed β no AI involved.
Nightdrop is only a hard security boundary when these requirements are true:
| Requirement | Why it matters |
|---|---|
| Nightdrop runs as a separate OS user | Prevents the AI agent process from reading/modifying pending or approved drafts |
| The agent has dropbox access to the inbox | The agent cannot enumerate the inbox or access service-claimed pending/ state. It still controls its own known inode until Nightdrop captures a detached private copy; removal of the unchanged inbox entry is a separate best-effort cleanup step. |
| Send credentials live only in Nightdrop | If the agent also has SMTP, Gmail, Outlook, or Zoho send credentials, it can bypass the gate |
| The approval bot token is not available to the agent | Prevents the agent from approving its own drafts |
| Gmail/Outlook mailbox access is exposed only through the bounded Unix-socket broker | Keeps Gmail App Passwords, Outlook OAuth tokens, arbitrary IMAP commands, and unrestricted Graph calls outside the agent process |
security.enforceProductionPermissions: true in production |
Startup fails closed unless queue path types, required modes, service ownership, and the root-owned draft parent are correct |
| Humans review the actual action | The approval decision is about the concrete email payload or exact mailbox operation shown in Telegram |
If you run Nightdrop and your agent as the same Unix user, or give the agent direct send credentials, Nightdrop is still useful as an approval workflow β but it is not a structural security boundary.
See docs/deployment.md for the production filesystem setup, docs/hermes.md for bounded mailbox and outbound Hermes integration, docs/credential-handoff.md for operator responsibilities, docs/smtp-onboarding.md for simple Gmail App Password setup, docs/mailbox-cleanup.md for human-gated Spam/Trash unread cleanup, and docs/oauth-onboarding.md for narrower OAuth authorization.
Hermes Agent already has useful approval controls for tool and command risk. Nightdrop is complementary, not a replacement.
| Capability | Hermes built-in approval | Nightdrop |
|---|---|---|
| Primary approval target | Tool calls / shell commands | Exact email payloads and mailbox operations |
| Typical question | βShould this command/tool run?β | βShould this email be sent, or should this exact message be moved or unsubscribed?β |
| Final executor | Hermes tool runtime | Separate deterministic service |
| Send credentials | May live in Hermes if configured | Live only in Nightdrop |
| Best for | Dangerous local commands, tool use, operational actions | Email, replies, and bounded mailbox actions |
| Security shape | Tool-level/behavioral approval | Payload-level structural boundary |
For example, Hermes approval can help decide whether a risky command should run. Nightdrop is for a different problem: Hermes drafts an email, but a separate process with separate credentials sends only the exact payload a human approved.
This isn't "we told the AI to be careful." It's structural:
- Process isolation β Nightdrop runs as a separate OS user. The AI agent cannot read, modify, or delete drafts after submission.
- Dropbox inbox β the agent can create a known submission but cannot enumerate the directory (Unix dropbox permissions:
1730). The watcher opens it without following symlinks, validates a bounded snapshot, writes a new exclusive0600service-owned inode in privatepending/, and only then removes the unchanged inbox entry. Retained hard links or open file descriptors cannot mutate the claimed copy. - Hash-verified approvals β a full SHA-256 hash is computed at preview time and bound to an unguessable approval nonce. If the draft is modified between preview and approval, the approval is rejected.
- From-address enforcement β the
fromfield in drafts is ignored. The approval preview shows the configured sender address and labels the draftfromas ignored, preventing approval-screen spoofing. - No AI in execution β the executor reads the approved file directly. No LLM processes, summarizes, or touches the content.
- Out-of-band approval β the Telegram bot has its own token and runs independently. The AI agent has no access to it.
- Schema validation β Zod schemas enforce bounds on all fields (subject: 500 chars, body: 256KB, tags: 20 max).
- Symlink and inode-race rejection β only bounded regular files opened with
O_NOFOLLOWare accepted; the opened inode must match the inspected inbox entry. Symlinks, devices, sockets, directories, and path swaps are rejected. - Sanitized errors β no raw API responses or stack traces leak to Telegram or audit logs.
This path is for local evaluation and development. It does not create the separate-user security boundary described above. Use docs/deployment.md and the installed credential-onboarding wrappers for production.
git clone https://github.com/404Dealer/nightdrop.git
cd nightdrop
npm ci
cp config.example.yaml config.yaml
# Set the environment variables referenced by config.yaml.
npm run build
npm start- Message @BotFather on Telegram
- Send
/newbotand follow the prompts - For local development, resolve the bot-token placeholder through your environment. For production, use
sudo /opt/nightdrop/scripts/configure-provider-secrets.sh telegram; do not place a literal token in config. - Get your Telegram user ID (message @userinfobot) and add it to
allowedUsers - Send
/startto your new bot
npm run dev # Runs with tsx, no build step neededIf your agent can write a JSON file, it can use Nightdrop. No SDK, no API client, no runtime dependency.
Drop a .json file into the configured inbox directory:
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"type": "email",
"status": "pending",
"createdAt": "2026-01-15T10:30:00Z",
"updatedAt": "2026-01-15T10:30:00Z",
"source": "my-agent",
"provider": "zoho",
"payload": {
"from": "ignored@example.com",
"to": "recipient@example.com",
"subject": "Follow-up on our conversation",
"body": "<p>Hi, just following up on our meeting yesterday.</p>"
},
"metadata": {
"context": "User asked me to follow up after yesterday's call",
"priority": "normal",
"tags": ["follow-up"]
}
}That's it. Nightdrop handles the rest.
| Framework | Integration |
|---|---|
| OpenClaw | Write draft file from agent session |
| LangChain | Custom tool that writes JSON |
| AutoGen | Function call that drops a file |
| Claude Code / Codex | Shell command to write JSON |
| Bash scripts | cat > drafts/inbox/draft.json << 'EOF' |
| Anything else | If it can write a file, it works |
| Field | Type | Required | Notes |
|---|---|---|---|
id |
UUID string | β | Unique draft identifier |
type |
"email" | "webhook" |
β | webhook is schema-reserved; no executing webhook provider ships yet |
status |
"pending" |
β | Must be "pending" for new drafts |
createdAt |
ISO 8601 | β | |
updatedAt |
ISO 8601 | β | |
source |
string | β | Identifies which agent/system created this |
provider |
string | β | Provider key from your config |
payload |
object | β | Provider-specific (see below) |
metadata.context |
string | Why the agent drafted this (shown in preview) | |
metadata.priority |
string | "normal", "high", etc. |
|
metadata.tags |
string[] | Up to 20 tags |
| Field | Type | Required | Limits |
|---|---|---|---|
to |
string or string[] | β | Valid email(s) |
subject |
string | β | Max 500 chars |
body |
string | β | Max 256KB, HTML supported |
cc |
string[] | ||
bcc |
string[] | ||
replyTo |
string |
Note: The
fromfield is accepted but ignored β the configuredfromAddresson the provider is always used.
Status flow: pending β approved / denied β sent / failed
telegram:
botToken: "${NIGHTDROP_BOT_TOKEN}"
allowedUsers: [123456789] # Your Telegram user ID(s)
watch:
directory: "./drafts/inbox"
pollIntervalMs: 2000
approval:
bodyPreviewChars: 2000 # Long bodies are truncated in Telegram previews
allowTruncatedApproval: false # Safer default: truncated drafts show Deny only
security:
enforceProductionPermissions: false # Set true after applying docs/deployment.md
providers:
gmail-smtp:
type: "email-smtp"
host: "smtp.gmail.com"
port: 465
tlsMode: "implicit"
username: "you@gmail.com"
password: "${NIGHTDROP_GMAIL_APP_PASSWORD}"
fromAddress: "you@gmail.com"
displayName: "Your Name"
gmail:
type: "email-gmail"
clientId: "${NIGHTDROP_GOOGLE_CLIENT_ID}"
# Desktop/public-client OAuth does not use clientSecret.
refreshToken: "${NIGHTDROP_GOOGLE_REFRESH_TOKEN}"
fromAddress: "you@gmail.com"
displayName: "Your Name"
outlook:
type: "email-outlook"
clientId: "${NIGHTDROP_MICROSOFT_CLIENT_ID}"
# Omit clientSecret for public-client PKCE/device flows.
refreshToken: "${NIGHTDROP_MICROSOFT_REFRESH_TOKEN}"
tenantId: "common"
fromAddress: "you@outlook.com"
displayName: "Your Name"
zoho:
type: "email-zoho"
clientId: "${NIGHTDROP_ZOHO_CLIENT_ID}"
clientSecret: "${NIGHTDROP_ZOHO_CLIENT_SECRET}"
refreshToken: "${NIGHTDROP_ZOHO_REFRESH_TOKEN}"
region: "us"
accountId: "${NIGHTDROP_ZOHO_ACCOUNT_ID}"
fromAddress: "you@yourdomain.com"
log:
type: "log-only"
mailboxProfiles:
personal:
provider: gmail-smtp
defaults:
provider: "log"
timezone: "UTC"
autoDeleteAfterDays: 30
audit:
enabled: true
logFile: "./audit.log"Each mailbox profile maps one account to one provider. The base outlook provider above is send-only. A manually configured named Outlook mailbox is an explicit opt-in:
providers:
outlook-work:
type: "email-outlook"
clientId: "${NIGHTDROP_MICROSOFT_WORK_CLIENT_ID}"
refreshToken: "${NIGHTDROP_MICROSOFT_WORK_REFRESH_TOKEN}"
tenantId: "common"
fromAddress: "you@outlook.com"
mailboxAccess: true # Requires delegated Mail.ReadWrite.
mailboxProfiles:
work:
provider: outlook-workFor production, run sudo /opt/nightdrop/scripts/oauth-setup.sh outlook --profile work. The installed helper requests the required mailbox scope and creates the provider/profile binding atomically; you do not need to edit private production config by hand.
Config placeholders support two resolvers:
| Syntax | Source | Example |
|---|---|---|
${NIGHTDROP_VAR_NAME} |
Environment variable | ${NIGHTDROP_BOT_TOKEN} |
${PASS:path} |
pass (Unix password manager) | ${PASS:nightdrop/bot-token} |
Unresolved placeholders cause a hard failure at startup. No silent empty strings.
For the Himalaya-style self-hosted path, no Google Cloud project or OAuth client is required:
# Human-controlled terminal only
sudo /opt/nightdrop/scripts/configure-provider-secrets.sh telegram
# Existing one-account compatibility setup:
sudo /opt/nightdrop/scripts/smtp-setup.sh gmail
# Or create a named Gmail mailbox (repeat with a unique profile per account):
sudo /opt/nightdrop/scripts/smtp-setup.sh gmail --profile personalThe SMTP helper verifies Gmail over TLS, stores the App Password directly under nightdrop, writes only a versioned ${PASS:...} reference to private config, and restarts the service. App Passwords are simpler but broader than the Gmail API gmail.send scope. See docs/smtp-onboarding.md for prerequisites, revocation, and exact behavior.
After Gmail SMTP onboarding, an operator can clear unread badges in Gmail's Spam and Trash folders without exposing mailbox credentials to Hermes:
sudo /opt/nightdrop/scripts/mailbox-cleanup.sh gmailThe helper previews counts, requires the exact phrase MARK READ, and adds only \Seen to the snapshotted unread UIDs. It never deletes, moves, empties, or displays messages. See docs/mailbox-cleanup.md for UID snapshot semantics, auditing, partial outcomes, and troubleshooting.
The production installer exposes one fixed Unix-socket client without giving Hermes Gmail App Passwords, Microsoft refresh tokens, arbitrary IMAP, or arbitrary Graph access:
/usr/local/bin/nightdrop-mailbox profiles
/usr/local/bin/nightdrop-mailbox list --profile personal --unread --limit 20
/usr/local/bin/nightdrop-mailbox list --profile work --unread --limit 20
/usr/local/bin/nightdrop-mailbox read MESSAGE_REF
/usr/local/bin/nightdrop-mailbox mark-read MESSAGE_REF [MESSAGE_REF ...]
/usr/local/bin/nightdrop-mailbox propose-trash MESSAGE_REF [MESSAGE_REF ...] --context 'Why these messages are unwanted'
/usr/local/bin/nightdrop-mailbox propose-unsubscribe MESSAGE_REF --context 'Why this subscription should stop'Each named profile points to one Gmail SMTP/App Password provider or one Outlook provider. With one configured profile, list may omit --profile; with multiple profiles it must select one explicitly. Returned opaque references bind the profile and backend, so read, mark-read, Trash, and unsubscribe route back to the same account. Gmail references bind UIDVALIDITY + UID; Outlook references use Graph immutable message IDs. Legacy Gmail v1 references remain bound only to the unique gmail-smtp compatibility provider, whether that provider is exposed as implicit default or explicitly named; all other named Gmail providers reject them. Reading does not mark mail read. Mark-read affects at most 20 exact references. Mixed-profile bulk requests fail closed. Trash moves and unsubscribe requests require hash-bound Telegram approval.
Unsubscribe uses only authoritative List-Unsubscribe headers: RFC 8058 HTTPS one-click is preferred, with a strict RFC 2369 unsubscribe email fallback. It never follows links from the message body. HTTPS redirects, cookies, private-network targets, and caller-supplied URLs are unavailable. Permanent deletion and EXPUNGE remain unavailable.
A production installation can acquire provider refresh tokens without giving them to Hermes:
# Human-controlled terminal only
sudo /opt/nightdrop/scripts/configure-provider-secrets.sh telegram
# Then authorize one email provider:
sudo /opt/nightdrop/scripts/oauth-setup.sh gmail
sudo /opt/nightdrop/scripts/oauth-setup.sh outlook
sudo /opt/nightdrop/scripts/oauth-setup.sh outlook --profile work # bounded Inbox + send
sudo /opt/nightdrop/scripts/oauth-setup.sh zohoThe approval-bot token must exist first because the OAuth wrapper restarts the service after a successful commit. The helper drops privileges to nightdrop with a clean environment, uses state-bound PKCE browser authorization, writes tokens directly to the encrypted password store, atomically updates private config with versioned ${PASS:...} references, and restarts the service. Gmail, Outlook, and Zoho use SSH-forwarded loopback callbacks by default. Outlook device authorization is available only as an explicit --device-code fallback.
See docs/oauth-onboarding.md for provider registration and exact commands.
Dry-run provider. Logs the payload to stdout, sends nothing. Use for testing and development.
Sends through an authenticated SMTP server with mandatory certificate-verified TLS. Use tlsMode: implicit for TLS from connection start (commonly port 465) or tlsMode: starttls for a required STARTTLS upgrade (commonly port 587). Unencrypted SMTP is not supported.
| Config Key | Description |
|---|---|
host |
SMTP DNS name or IP address |
port |
SMTP port (1β65535) |
tlsMode |
implicit or starttls |
username |
SMTP authentication username |
password |
SMTP/App Password; use a ${PASS:...} reference in production |
fromAddress |
Enforced sender address; draft from is ignored. Must match username by default. |
allowFromAlias |
Optional explicit opt-in for an operator-verified sender alias or non-email SMTP username; default false |
displayName |
Optional display name shown in approval preview and From header |
The transport uses fixed connection/greeting/socket timeouts, disables file and URL access, and redacts provider errors. Partial SMTP acceptance is archived as non-retryable sent state, recorded as partial in audit, and shown to the operator as an explicit Telegram warning with accepted/rejected counts and safe rejected addresses. If SMTP accepts delivery but local archive/audit finalization fails, Telegram instead reports that delivery was accepted with a record warning and instructs the operator not to retry. These states prevent duplicate delivery to already accepted recipients.
Sends email via the Gmail API using OAuth refresh token flow. The OAuth client needs the https://www.googleapis.com/auth/gmail.send scope.
| Config Key | Description |
|---|---|
clientId |
Google Cloud OAuth client ID; Desktop client recommended |
clientSecret |
Optional legacy/confidential-client secret; omitted by secure Desktop onboarding |
refreshToken |
OAuth refresh token with Gmail send scope |
fromAddress |
Enforced sender address or configured Gmail send-as alias |
displayName |
Optional display name shown in From header |
Sends email via Microsoft Graph sendMail using Microsoft Entra OAuth refresh token flow. Send-only onboarding requests delegated offline_access, Mail.Send, and onboarding-only User.Read. Named mailbox-profile onboarding also requests Mail.ReadWrite for the bounded Inbox adapter.
| Config Key | Description |
|---|---|
clientId |
Microsoft Entra app/client ID |
clientSecret |
Optional confidential-client secret; omitted for recommended public-client PKCE/device flows |
refreshToken |
OAuth refresh token with offline_access Mail.Send |
refreshTokenKey |
Password-store key used to persist Microsoft token rotation; when set, refreshToken must be the exact matching ${PASS:key} reference. Legacy env/literal configs may omit it and rotate only in memory until restart. |
tenantId |
Tenant ID, or common for personal/multi-tenant auth |
userId |
Optional mailbox/user id; omitted uses /me/sendMail |
fromAddress |
Enforced sender address shown in approval preview |
displayName |
Optional display name shown in approval preview |
mailboxAccess |
Set by named Outlook mailbox onboarding; requires delegated Mail.ReadWrite and enables only the bounded Inbox adapter |
Sends email via the Zoho Mail API using OAuth refresh token flow.
| Config Key | Description |
|---|---|
clientId |
Zoho API Console client ID |
clientSecret |
Zoho API Console client secret |
refreshToken |
Long-lived refresh token (ZohoMail.messages.CREATE scope) |
region |
Pinned Zoho data center: us, eu, in, au, jp, ca, or sa (default us) |
accountId |
Zoho account ID |
fromAddress |
Enforced sender address (overrides draft from) |
Implement the Provider interface and register it:
// src/providers/my-provider.ts
import type { Draft } from '../schema.js';
import type { Provider, ProviderResult } from './index.js';
export class MyProvider implements Provider {
async send(draft: Draft): Promise<ProviderResult> {
// Send the draft however you want
return { details: 'Sent via my service' };
}
}Register in src/providers/index.ts. PRs for new providers welcome.
When a draft arrives:
π§ New Email Draft
From: you@yourdomain.com
To: recipient@example.com
Subject: Follow-up on our conversation
βββββββββββββ
Hi, just following up on our meeting yesterday.
βββββββββββββ
Source: my-agent
Context: User asked me to follow up
Priority: normal
[β
Approve] [β Deny]
- Approve β hash verified β executed β
β APPROVED at 3:45 PM - Deny β draft archived β
β DENIED at 3:45 PM
If the draft was modified after the preview was sent, approval is rejected with a warning.
For maximum isolation, run Nightdrop as a dedicated system user:
- Create a service user β no login shell, locked home directory
- Set up credentials β dedicated
passstore for the service user - Inbox as dropbox β sticky bit + group write, no read (
1730) - systemd service β hardened with
NoNewPrivileges,ProtectSystem=strict, restricted address families - Audit log β private by default; an operator may explicitly grant a read-only ACL with
--grant-agent-audit-read
See docs/deployment.md for the complete production hardening guide with copy-paste commands.
nightdrop/
βββ src/
β βββ index.ts # Entry point
β βββ config.ts # Config loader + secret resolution
β βββ oauth-setup.ts # Human-only PKCE OAuth CLI (device fallback for Outlook)
β βββ oauth/ # PKCE, callback, provider OAuth, secure persistence
β βββ watcher.ts # File watcher (detached inbox capture β pending)
β βββ bot.ts # Telegram bot (previews + callbacks)
β βββ executor.ts # Reads approved drafts, dispatches to providers
β βββ schema.ts # Zod schemas + validation
β βββ mailbox-broker/ # Named Gmail/Outlook INBOX, Trash, and unsubscribe broker
β βββ providers/
β βββ index.ts # Provider registry
β βββ email-smtp.ts # Authenticated SMTP / Gmail App Passwords
β βββ email-gmail.ts # Gmail API
β βββ email-outlook.ts # Microsoft Graph sendMail
β βββ outlook-token-client.ts # Shared Microsoft token rotation and Graph client
β βββ email-zoho.ts # Zoho Mail API
β βββ log-only.ts # Dry-run logger
βββ drafts/ # Draft queue directories
β βββ inbox/ # Public dropbox (agents write here)
β βββ pending/ # Internal (watcher writes detached private copies here)
β βββ approved/ # Human-approved
β βββ sent/ # Successfully executed
β βββ denied/ # Human-denied
β βββ failed/ # Validation or send errors
βββ docs/
β βββ credential-handoff.md # Operator and agent credential boundaries
β βββ deployment.md # Production hardening guide
β βββ hermes.md # Hermes mailbox and approval workflow
β βββ mailbox-cleanup.md # Gmail Spam/Trash unread cleanup
β βββ oauth-onboarding.md # Gmail, Outlook, and Zoho OAuth setup
β βββ smtp-onboarding.md # Gmail App Password setup
βββ config.example.yaml
βββ package.json
βββ tsconfig.json
βββ LICENSE
- β File-based draft queue (inbox β pending β approved β sent)
- β Telegram bot with inline approve/deny buttons
- β SHA-256 hash-verified approvals with nonce-bound callbacks
- β Gmail email provider
- β Authenticated TLS SMTP provider and Gmail App Password onboarding
- β Outlook / Microsoft Graph email provider
- β Zoho Mail email provider
- β
Direct-to-
nightdropbrowser/device OAuth onboarding for Gmail, Outlook, and Zoho - β Log-only dry-run provider
- β
${PASS:key}and${NIGHTDROP_*}secret resolvers - β Schema validation with size/count bounds
- β JSON audit logging
- β Symlink/device file rejection
- β From-address enforcement in execution and approval preview
- β Safe long-body preview policy (truncated drafts deny-only by default)
- β Optional production permission checks at startup
- β Credential-isolated named Gmail/Outlook Inbox list/read/mark-read broker for Hermes
- β Telegram-approved Gmail Trash and Outlook Deleted Items moves with no permanent-delete path
- β Standards-based HTTPS/mailto unsubscribe with no body-link execution
- β Human-confirmed Gmail Spam/Trash unread cleanup with no message deletion or display
- Edit flow β modify drafts in Telegram before approving
- Webhook provider (for non-email actions: Slack, Discord, APIs)
- Bulk approve/deny
- Draft expiry (auto-deny after configurable timeout)
- Rate limiting / cooldowns
- Web dashboard for audit trail
"Agents propose, humans approve via out-of-band channel, deterministic scripts execute."
The agent security space is full of behavioral guardrails β system prompts, content filters, output classifiers. These are valuable but fundamentally brittle: they depend on model compliance, which prompt injection can subvert.
Nightdrop takes a different approach: structural security. The approval gate is a separate process, running as a separate user, with its own credentials. When the deployment requirements above hold, prompt injection alone cannot make the AI agent approve its own drafts because the agent cannot access the approval channel or execution credentials.
This is the same principle behind air-gapped networks, hardware security modules, and two-person integrity controls β applied to AI agents.
The skill/ directory contains an agent skill (compatible with OpenClaw, NanoClaw, and any framework that uses SKILL.md-based skill loading):
skill/
βββ SKILL.md # Instructions for the AI agent
βββ scripts/
βββ draft-email.sh # Helper script for drafting emails
The skill teaches an AI agent how to write properly-formatted draft files. The agent learns the schema, constraints, and workflow β then uses sg nightdrop-inbox (or the helper script) to drop drafts into the inbox.
Install the skill in your agent framework, point it at your Nightdrop inbox, and your agent can propose emails that you approve via Telegram.
See docs/hermes.md for a dedicated Hermes setup guide.
Nightdrop is intentionally designed as a Hermes-compatible outbound-action gate:
- Install this repo's
skill/directory as a Hermes skill, or keep it in a project and load it for sessions that may draft email. - Give Hermes write-only access to
/opt/nightdrop/drafts/inboxthrough thenightdrop-inboxgroup. - Do not give Hermes the SMTP/API credentials that Nightdrop uses to send. Hermes should read/search/draft; Nightdrop should send.
- For a draft request, Hermes writes JSON only and then tells the user it is pending approval. The approved send happens outside the Hermes process.
That separation is what makes the gate stronger than an agent asking, βShould I send this?β and then calling a send tool itself.
See CONTRIBUTING.md for guidelines.
MIT β see LICENSE