← Portfolio/free-code

free-code

Inbound webhooks — let external systems (Flowise, CI, GitHub) trigger your agent.

What are inbound webhooks?

free-code opens a small local HTTP endpoint so external systems can push events into a running agent session.

MCP lets free-code call out to external tools. Inbound webhooks are the opposite direction: an external system — a Flowise flow, a CI job, a GitHub event — makes an HTTP POST to free-code, and that event either lands in a queue the agent can read or wakes the agent to act on it immediately.

Why this is an extension, not an MCP server
An MCP server can't start an agent turn — it only answers when called. Waking the agent from an incoming event needs the extension API (sendUserMessage()), so this ships as the built-in webhook-receiver extension, loaded automatically in every session.

Watch: Inbound webhooks in free-code

See an external system trigger a running agent session.

How it works

Register a named hook, hand its URL to the external system, receive its POSTs.

  1. You register a named hook (for example flowise) from inside free-code.
  2. free-code exposes it at POST http://127.0.0.1:<port>/hook/<name> and returns a secret.
  3. You configure the external system (Flowise, GitHub, …) to POST to that URL with the secret.
  4. When a request arrives and passes authentication, the event is delivered to the agent.

Each hook has a delivery mode:

ModeBehaviourBest for
queue The event is buffered. The agent drains it with poll_webhook_events or long-polls with wait_for_webhook_event. Predictable, on-demand consumption; noisy sources.
trigger The event is injected as a message that starts a turn immediately (queued as a follow-up if the agent is mid-stream). React-now events: a build failed, a flow finished.
The endpoint lives only while free-code runs
Registered hooks persist in a config file, but the listener is only up while a session is open. Buffered events are held in memory, not on disk — for an always-on receiver you'd front it with a dedicated broker process.

Flow diagrams

The direction of data, the end-to-end lifecycle, and what happens to each event.

1 · Direction — the reverse of MCP

MCP — free-code calls out free-code tool call → MCP server Webhook — an external system calls in External system POST event → free-code
MCP tools go out from free-code; webhooks bring events in. Only an extension (not an MCP server) can wake the agent from an inbound event.

2 · Lifecycle — from registration to the agent

1 Register hook /webhook add name 2 Share URL + secret to system 3 System POSTs to /hook/name 4 Authenticate & deliver to agent
Steps 1–2 happen once. Steps 3–4 repeat for every incoming event while a session is open.

3 · Delivery — what happens to each event

POST /hook/:name Authenticate secret / HMAC 401 rejected queue mode Buffer event in the queue Agent drains poll / wait tool trigger mode sendUserMessage starts a turn Agent wakes immediately
Authentication is mandatory. A valid event then follows its hook's mode: queue buffers for later, trigger wakes the agent now.

What you can do with it

  • Let a Flowise flow drive the agent — a flow's HTTP node calls back into free-code when it finishes.
  • React to CI/CD — a failed build POSTs its log and the agent starts investigating.
  • Bridge SaaS events — GitHub, Stripe, or any service that emits webhooks (via a tunnel, see below).
  • Collect asynchronous callbacks — kick off long-running external work, then wait_for_webhook_event for the result.
  • Register the callback for you — the agent can POST free-code's URL into the external system's own API.

Using it

Register from the command line, or just ask the agent.

With the /webhook command (CLI)

# Register a hook that wakes the agent on every event
/webhook add flowise --trigger

# Show its callback URL + secret again
/webhook url flowise

# List hooks and server status
/webhook list

# Peek at buffered events (queue mode)
/webhook events

# Force a specific port for this session
/webhook port 8790

# Remove a hook
/webhook rm flowise

Omit --trigger to create a queue hook. A secret is generated automatically.

By asking the agent (works everywhere, including the plugin)

The same actions are exposed as tools, so in the CLI, the IDE plugin, or the desktop app you can just say: "register a webhook called flowise in trigger mode and give me the URL."

Plugin & desktop
The tools work on every surface. The typed /webhook command is also listed in the plugin and desktop command catalogs.

Testing it

Send a request with curl and watch the agent receive it.

After /webhook add test --trigger, take the URL and secret it printed, then:

# Replace the port and secret with what /webhook url test showed
curl -X POST http://127.0.0.1:8788/hook/test \
  -H 'content-type: application/json' \
  -H 'X-Webhook-Secret: <your-secret>' \
  -d '{"msg":"hello from curl"}'

Expected: the request returns {"ok":true}, and in a trigger hook the agent wakes and reacts to the payload.

For a queue hook, drain what arrived:

/webhook events        # or ask the agent to poll the webhook events

Auth is mandatory — a wrong or missing secret is rejected with 401:

curl -i -X POST http://127.0.0.1:8788/hook/test \
  -H 'X-Webhook-Secret: wrong' -d '{}'
# HTTP/1.1 401 Unauthorized

There's also a liveness endpoint:

curl http://127.0.0.1:8788/health
# {"ok":true,"hooks":["test"]}

Security

Local by default; every hook is authenticated.

  • Binds 127.0.0.1 by default — not reachable from the network.
  • Every hook requires a credential. A hook with no secret never authenticates.
  • Shared secret — send it in the X-Webhook-Secret header (compared in constant time).
  • HMAC — set an hmacSecret and sign the raw body: X-Signature: sha256=<hex>.
  • Body size is capped (1 MB by default) to reject oversized payloads.
Exposing it to cloud services
For sources that aren't on your machine (GitHub, Stripe, …), front the endpoint with a tunnel such as cloudflared tunnel --url http://127.0.0.1:8787 or ngrok, set publicBaseUrl, and require HMAC.

Multiple sessions & ports

Every free-code window gets its own port automatically.

Each session binds the first free port from 8787 upward (8787, 8788, 8789, …), so several free-code windows coexist without clashing. The callback URL always reflects the port a session actually bound — check it with /webhook url <name>.

To pin a known port:

# Per session, live:
/webhook port 8790

# Or set it before launch:
FREECODE_WEBHOOK_PORT=8790 free-code

Reference

Tools, config file, and environment variables.

Tools

ToolWhat it does
register_webhookCreate a named hook and return its callback URL + secret.
unregister_webhookRemove a hook.
get_webhook_urlShow a hook's callback URL and auth hint.
list_webhooksList hooks, server status, and queued-event count.
poll_webhook_eventsReturn (and by default drain) buffered events.
wait_for_webhook_eventLong-poll until the next event or a timeout.
register_external_webhookPOST free-code's callback URL into an external system's API.

Config file — ~/.free-code/agent/webhook-receiver.json

{
  "enabled": true,
  "host": "127.0.0.1",
  "port": 8787,
  "portRange": 20,
  "publicBaseUrl": null,
  "defaultMode": "queue",
  "maxBodyBytes": 1048576,
  "hooks": {
    "flowise": { "mode": "trigger", "secret": "…" }
  }
}

Environment overrides

VariableEffect
FREECODE_WEBHOOK_ENABLEDTurn the receiver on/off.
FREECODE_WEBHOOK_PORTBase port to bind.
FREECODE_WEBHOOK_HOSTBind host (default 127.0.0.1).
FREECODE_WEBHOOK_PUBLIC_URLPublic base URL used in callback URLs (behind a tunnel).