<!--
Sitemap:
- [Installation](/installation)
- [Upgrading](/upgrading): Version-specific steps for upgrading an existing Bento install.
- [Concepts](/concepts)
- [Build your first pipeline](/tutorials/pipeline-args)
- [Target a specific issue or PR from a URL](/tutorials/url-targeting)
- [Keep state across runs](/tutorials/pipeline-state)
- [Fire a pipeline on a schedule or on demand](/tutorials/schedule-and-fire)
- [Deploy a box to Railway](/tutorials/deploy-to-railway)
- [Operate a hosted daemon](/tutorials/operate-a-hosted-daemon)
- [Configuration](/configuration)
- [Members](/members)
- [Knowledge base](/knowledge-base/)
- [Method and delivery](/knowledge-base/modes)
- [Config](/knowledge-base/config)
- [MCP](/knowledge-base/mcp)
- [Pipeline configuration reference](/pipelines/config)
- [Filters](/pipelines/filters)
- [Triggers](/triggers/)
- [GitHub trigger](/triggers/github)
- [Linear trigger](/triggers/linear)
- [Webhook trigger](/triggers/webhook)
- [Schedule trigger](/triggers/schedule)
- [Manual trigger](/triggers/manual)
- [Traces](/pipelines/traces)
- [Slack](/integrations/slack)
- [Public access](/public-access)
- [Context engineering](/context-engineering)
- [Best practices](/best-practices)
- [Troubleshooting](/troubleshooting)
- [Architecture](/architecture/vision)
- [Workspaces](/workspaces)
- [Authentication](/authentication)
- [Identity](/identity)
- [Security](/security)
- [References](/references)
- [Changelog](/changelog): Bento release history.
- [CLI reference](/cli/)
- [Setup](/cli/setup)
- [Secrets](/cli/secrets)
- [Lifecycle](/cli/lifecycle)
- [Sandbox image](/cli/image)
- [Sandboxes](/cli/sandbox)
- [Observability](/cli/observability)
- [Diagnostics](/cli/diagnostics)
- [Triggers](/cli/triggers)
- [Workbench](/cli/workbench)
- [Auth](/cli/auth)
- [Knowledge](/cli/knowledge)
- [Evals](/cli/evals)
- [Bento](/index)
- [Runtime wrapper](/architecture/runtime-wrapper)
- [Skill evolve](/architecture/skill-evolve)
-->

# Public access

The daemon binds to `127.0.0.1:7890` by default. To accept GitHub webhooks or reach the daemon from another machine, expose it through a tunnel.

| | Cloudflare Quick Tunnel | Tailscale Funnel |
|---|---|---|
| **Account required** | No | Yes (free) |
| **URL stability** | Rotates on restart | Persistent (`*.ts.net`) |
| **Best for** | One-off testing, demos | Production webhooks, daily use |
| **Prerequisite** | `cloudflared` on PATH | Tailscale enrolled on host |

***

## Cloudflare Tunnel

Bento starts a [Cloudflare Quick Tunnel](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/do-more-with-tunnels/trycloudflare/) beside the daemon and gives you an ephemeral `*.trycloudflare.com` URL. You need no Cloudflare account. Use it for a one-off webhook receiver or a demo.

:::note
Quick Tunnel URLs are public. The bearer-token middleware of the daemon and the webhook signature verification are the only authentication. Never disable them.
:::

### Prerequisites

* `cloudflared` on the daemon host's `PATH`. On macOS: `brew install cloudflared`. On Linux see [Cloudflare's install docs](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/).
* `bento doctor` validates this when the `tunnel:` block is present in `.bento/daemon.yaml`.

### Setup

Add a `tunnel:` block to `.bento/daemon.yaml`:

```yaml
tunnel:
  provider: cloudflare
  mode: quick
```

Then restart the daemon:

```bash
bento daemon restart
```

The daemon spawns `cloudflared`, parses the public URL out of its startup log, and emits one info line:

```
[INFO] [tunnel] public URL ready: https://essentially-carrying-recorder-merely.trycloudflare.com
```

`bento daemon status` surfaces the same URL:

```
local   : reachable (8ms) — @bento/daemon uptime=14s
tunnel  : reachable (83ms) — https://essentially-carrying-recorder-merely.trycloudflare.com
funnel  : not configured (tailscale funnel not active)
```

:::warning
Quick Tunnel URLs **rotate on every daemon restart**. For a stable URL use a [named tunnel](#named-tunnels-stable-url) or Tailscale Funnel.
:::

### Use the public URL

**GitHub webhooks** — point the webhook delivery URL at:

```
https://<random>.trycloudflare.com/events
```

Make sure the `github` source's secret in `.bento/daemon.yaml` matches the GitHub webhook secret so signature verification passes. The daemon also accepts `/webhooks/github` and `/webhooks/linear`.

A tunnel makes every webhook route public, so declare each source it serves under `webhooks.sources`. The daemon treats a configured tunnel as reachable off the machine and refuses to start if a webhook-triggered pipeline has no usable source, even on a loopback bind. See [Configuration](/configuration#webhooks).

**MCP clients** — configure Claude Code, Codex, or pi against the tunnel URL. See [Authentication](/authentication) for the exact commands. Issue a bearer token first:

```bash
bento token issue --email you@example.com
```

### Lifecycle

* The daemon supervises the tunnel as a child process. `bento daemon stop` sends `SIGTERM` to `cloudflared` before it stops the HTTP server.
* If `cloudflared` crashes while the daemon is up, the daemon logs the exit code at `warn` and keeps running. Restart to re-establish the tunnel.
* The HTTP listener starts before the tunnel is ready. Queue workers wait for [startup readiness](/cli/lifecycle#startup-readiness), including the configured tunnel, before they claim jobs.

### Named tunnels (stable URL)

For a stable URL, pre-create a **named tunnel** in your Cloudflare account.

#### Prerequisites

* A free [Cloudflare account](https://dash.cloudflare.com/sign-up).
* `cloudflared` on the host.
* A hostname you control with a CNAME pointing at the tunnel:
  * **Cloudflare DNS** — `cloudflared tunnel route dns` creates the CNAME automatically.
  * **External DNS** — create `bento.example.com CNAME <tunnel-uuid>.cfargotunnel.com` yourself.

#### One-time setup

1. Authenticate `cloudflared`:

   ```bash
   cloudflared tunnel login
   ```

2. Create the tunnel:

   ```bash
   cloudflared tunnel create bento-prod
   ```

3. Route a hostname:

   ```bash
   # Cloudflare-managed DNS:
   cloudflared tunnel route dns bento-prod bento.example.com

   # External DNS: add a CNAME manually:
   # bento.example.com   CNAME   <uuid>.cfargotunnel.com
   ```

#### Point bento at the tunnel

```yaml
tunnel:
  provider: cloudflare
  mode: named
  name: bento-prod
```

Then `bento daemon restart`. The daemon runs `cloudflared tunnel run`. It reads the public hostname from the `ingress` rules in `~/.cloudflared/config.yml` — the rule whose `service` points at the daemon's own port — and it queries the Cloudflare DNS API when that file gives no hostname. It then requests `/health` on that hostname through the public route. It requires the answer to carry this daemon's boot identifier. A hostname that reaches a different Bento daemon does not count. `bento daemon status` shows the URL after that request reaches this daemon.

Until then, `bento daemon status` shows `tunnel : pending`, and the daemon log gives the reason: no hostname for the tunnel, or a public URL that does not answer.

#### Delete the tunnel

```bash
cloudflared tunnel delete bento-prod
```

Remove the DNS CNAME separately, through your DNS provider.

#### Files

| Path | Created by | Purpose | Needed at runtime? |
|---|---|---|---|
| `~/.cloudflared/cert.pem` | `tunnel login` | Manage tunnels (create/delete/route DNS) | No |
| `~/.cloudflared/<uuid>.json` | `tunnel create <name>` | Authenticate the running tunnel | **Yes** |
| `~/.cloudflared/config.yml` | hand-written (optional) | Ingress rules, and the hostname the daemon checks | Only if not passing `--url` |

### Notes

* Cloudflare may rate-limit Quick Tunnels under heavy traffic. For production webhook ingestion use a named tunnel.

***

## Tailscale Funnel

[Tailscale Funnel](https://tailscale.com/kb/1223/funnel) gives a persistent TLS-terminated `*.ts.net` URL routed back to your machine over the tailnet.

Bento manages the funnel, or you run it yourself:

* **Let bento manage it** — `tunnel:` block in `.bento/daemon.yaml`. Bento calls `tailscale funnel --bg <port>` at start and teardown.
* **Manage it yourself** — run `tailscale funnel --bg 7890` once. `bento daemon status` still detects it.

### Prerequisites

* A [Tailscale](https://tailscale.com) account with the daemon's host enrolled.
* HTTPS certificates and Funnel enabled for your tailnet (admin console: **DNS → HTTPS Certificates** and **Access Controls → Funnel**).

### Bento-managed mode

```yaml
tunnel:
  provider: tailscale
  mode: funnel
```

`bento daemon restart` execs `tailscale funnel --bg 7890`, polls `tailscale funnel status` for the public URL, and surfaces it through `bento daemon status`.

On shutdown bento runs `tailscale funnel --bg off`, but only when bento enabled the funnel itself. If a funnel is already active at startup, bento adopts the existing URL and skips both the setup and the teardown.

:::warning
`tailscale funnel --bg off` is **not port-scoped**, and it disables every funnel on the host. If you run several funnels on the same machine, set them up before you start bento, so that bento adopts them and leaves them alone. Otherwise, use manual mode.
:::

### Manual mode

```bash
tailscale funnel --bg 7890
```

`--bg` persists across shell exits. Run this once per machine.

Verify:

```bash
tailscale funnel status
# https://8640p.tail673878.ts.net (Funnel on)
# |-- / proxy http://127.0.0.1:7890
```

`bento daemon status` picks it up automatically:

```
local   : reachable (4ms) — bento uptime=120s
funnel  : reachable (38ms) — https://8640p.tail673878.ts.net
```

### Use the public URL

**MCP clients** — configure Claude Code, Codex, or pi — see [Authentication](/authentication). Issue a token first:

```bash
bento token issue --email you@example.com
```

**GitHub webhooks** — point the delivery URL at:

```
https://<node>.tail<id>.ts.net/events
```

Set the same secret as the `github` source in `.bento/daemon.yaml`.

### Stopping the funnel

```bash
tailscale funnel --bg off
```

The command unbinds the public URL and leaves the daemon running locally.

### Notes

* Funnel only serves ports `443`, `8443`, and `10000` on the public side. Tailscale maps `:443` → `:7890` automatically.
* Funnel traffic counts against tailnet bandwidth limits.
* The funnel URL is publicly reachable. Authentication is enforced by the daemon's bearer-token middleware — never disable it.
