Status: experimental. The hook protocol, environment variables, and payload fields may change or be removed in a future release without a major version bump. If you build integrations on this, pin a marchat version and watch release notes.
Client hooks run local executables you choose, fed one JSON object per event on stdin (newline-terminated). They are not server plugins: they only see what your client process sees, after decrypt on receive and before encrypt on send.
- Server plugins automate hub-side behavior and see server-wide trust boundaries.
- Built-in notifications cover in-TUI alerts with a fixed feature set.
- Client hooks fill the gap: pipe chat events to your scripts, loggers, bridges, or external notification systems without new client dependencies or server changes.
Hooks are side-effect only: they cannot modify, block, or transform messages (runs are asynchronous).
- Plaintext: A receive hook runs after the client decrypts; a send hook runs with the plaintext you typed. Anyone who can read hook logs or the hook binary's behavior gets the same material as the TUI. This does not weaken wire encryption (the server still sees ciphertext for E2E traffic); it widens local exposure to whatever you execute.
- Absolute paths only:
MARCHAT_CLIENT_HOOK_RECEIVEandMARCHAT_CLIENT_HOOK_SENDmust be absolute paths to a regular file. Relative paths are rejected to avoid surprisingPATH/ working-directory behavior. - Trust the binary: Hooks run as your user. Only point at programs you trust, same as running them manually.
- Timeouts: Each invocation is killed if it exceeds the configured timeout (default 5s, max 120s) so a stuck script does not pile up forever.
- File messages: Payloads include file metadata (
filename,size) only; raw file bytes are never sent to hooks.
| Variable | Meaning |
|---|---|
MARCHAT_CLIENT_HOOK_RECEIVE |
Absolute path to executable for inbound events (message_received). |
MARCHAT_CLIENT_HOOK_SEND |
Absolute path to executable for outbound composer sends (message_send). |
MARCHAT_CLIENT_HOOK_TIMEOUT_SEC |
Optional. Per-hook timeout in seconds (default 5, max 120). |
MARCHAT_CLIENT_HOOK_RECEIVE_TYPING |
Set to 1, true, or yes to deliver typing indicators to the receive hook. Default: off (reduces log noise). |
MARCHAT_CLIENT_HOOK_DEBUG |
Set to 1, true, or yes to log successful hook completion (duration) and label stdout as a debug preview. |
MARCHAT_HOOK_LOG |
Optional log file path for the bundled example_hook binary only. The marchat client does not read this variable. |
Unset hook paths mean that hook is disabled.
Run marchat-client -doctor (or -doctor-json) to inspect your client environment:
- The Environment section lists hook-related variables, including
MARCHAT_HOOK_LOGfor visibility, even though onlyexample_hookuses it (with the same masking and truncation rules as other doctor output). - If
MARCHAT_CLIENT_HOOK_RECEIVEorMARCHAT_CLIENT_HOOK_SENDis set, doctor runs a path check: the value must be an absolute path to an existing regular file, matching what the client enforces at runtime.
Server doctor does not list client-only hook variables, even if they are set in the environment (the server never reads them; hiding them avoids noise when client and server are run from the same shell).
- One UTF-8 JSON object per invocation, written to the process stdin, terminated with a single newline (
\n). - The client waits for the process to exit, up to the timeout. Stdout and stderr are captured; non-empty stdout is logged at INFO (see debug flag above). A non-zero exit or timeout is logged as a failure.
{
"event": "message_received | message_send",
"version": 1,
"message": { }
}version: Integer. Incremented when incompatible payload changes are introduced; until then, treat as1.event:message_received: Fired for each inboundshared.Messageon the chat WebSocket path after the client has applied decrypt when E2E is on. Typing is excluded unlessMARCHAT_CLIENT_HOOK_RECEIVE_TYPINGis enabled.message_send: Fired for outbound text from the main composer path: global plaintext, global E2E (plaintext before encrypt), DMs, and:server/admin lines sent asAdminCommandType. Other UI actions (e.g. code snippet, file picker) may not invoke the send hook.
Mirrors shared.Message fields where applicable, as a JSON object:
| Field | Notes |
|---|---|
type |
e.g. text, dm, typing, reaction, admin_command, … May be empty if the server omitted it on older history. |
sender, content, encrypted, message_id, recipient, edited, channel |
Same meaning as wire types. |
created_at |
RFC3339 nano UTC. Omitted when the client has no real timestamp (zero time), so you will not see 0001-01-01 sentinels. |
reaction |
Present for reaction events (emoji, target_id, is_removal). |
file |
For file messages: { "filename", "size" } only; no data. |
Filter scripts on message.type / event as needed.
Build the bundled sample (from repo root). The directory is named _example_hook on purpose: Go omits paths whose first path element begins with _ (or .) or is named testdata from ./..., so this sample package main tree is not matched by go test ./... at the repository root. You still build or run it by passing that path explicitly, as below.
go build -o /tmp/marchat-hook-log ./client/exthook/_example_hookRun the client with hooks (paths must be absolute on your OS):
export MARCHAT_CLIENT_HOOK_RECEIVE=/tmp/marchat-hook-log
export MARCHAT_CLIENT_HOOK_SEND=/tmp/marchat-hook-log
# Optional: override log path (otherwise uses $TMPDIR/marchat-client-hook.log)
export MARCHAT_HOOK_LOG=$HOME/marchat-hook.log
go run ./clientOn Windows (PowerShell), use Resolve-Path for absolute paths and run go run ./client from the repository root (not go run .).
- Custom logging or archival to your own storage
- Webhooks or bridges to other chat systems
- Keyword-triggered local alerts beyond built-in notifications
- Development and integration testing
| Mechanism | Where it runs | Typical use |
|---|---|---|
| Server plugins | Server | Commands, hub automation, shared bots |
| Client hooks | Your machine | Personal automation, local pipelines |
| Second WebSocket client | Your machine / elsewhere | Full bots, independent sessions |
Treat this document and the version field as the reference for experiments. For production-like integrations, prefer discussing on GitHub issues so breaking changes can be coordinated.