Get Moshi
deep dive

Debugging the gateway

Chat View, diff, and browser preview ride on a local gateway at 127.0.0.1:24543. When they go quiet, this is the page to walk through.

updated 1 week ago8 min readpage 38 / 40

The gateway runs on 127.0.0.1:24543. Its /events WebSocket streams terminal context, the detected agent session, git state, and dev servers to the app. Chat View opens a separate /v1/transcripts WebSocket after /events identifies the active agent and session. When Chat View, diff, or browser preview isn't working, this is the first thing to check.

Is the gateway alive?

probe the gateway
$curl -s http://127.0.0.1:24543/v1/diff/start
# Expected: 405 Method Not Allowed (GET not accepted, but proves the server is up)

If this times out, the daemon isn't running or the gateway failed to bind:

daemon checks
$brew services list | grep moshi
$tail -50 ~/Library/Application\ Support/Moshi/hook.log | grep gateway

Connect with wscat

Install wscat if needed (bun i -g wscat / bunx wscat, or npm i -g wscat / npx wscat).

No session (server discovery only)

discovery only
$wscat -c ws://127.0.0.1:24543/events

You'll get servers but context will be absent. Good enough to verify the gateway is broadcasting.

With SSH session

Find your SSH_CONNECTION value from inside the remote shell:

remote shell
$echo $SSH_CONNECTION
192.168.1.10 52431 192.168.1.20 22

Then connect:

ssh session
$wscat -c "ws://127.0.0.1:24543/events?session=ssh&sshConnection=192.168.1.10 52431 192.168.1.20 22"

With Mosh session

First, find the mosh-server's bind address and port:

find mosh-server
$lsof -i UDP | grep mosh
mosh-serv 67724 jyo 4u IPv4 ... UDP 100.96.116.120:60002

Then connect using the IP and port from the output:

mosh session
$wscat -c "ws://127.0.0.1:24543/events?session=mosh&moshHost=100.96.116.120&moshPort=60002"

If connecting over Tailscale MagicDNS, moshHost is the Tailscale IP (100.x.y.z), not the MagicDNS hostname.

Reading the response

A healthy connection returns a JSON message immediately:

{
  "context": {
    "kind": "tmux",
    "tmux": { "session": "main", "window": "1", "pane": "%5" },
    "cwd": "/home/user/project",
    "git": { "repo": "/home/user/project", "branch": "main", "dirty": true },
    "agent": {
      "name": "grok",
      "status": "working",
      "source": "tmux:process",
      "session": "019fab4f-d767-78f3-a8a5-163dbf623ad7",
      "sessionKind": "id",
      "pane": "%5"
    }
  },
  "servers": [
    {
      "id": "server_1",
      "name": "Vite",
      "host": "127.0.0.1",
      "port": 5173,
      "origin": "http://127.0.0.1:5173",
      "process": "node",
      "pid": 12345,
      "cwd": "/home/user/project",
      "isCurrentContext": true
    }
  ]
}

What to check

  • context missing — session lookup failed; wrong SSH_CONNECTION or moshHost:moshPort.
  • context.cwd missing — shell PID found but CWD unreadable.
  • context.git missing — CWD is not inside a git repo; diff will show "unavailable".
  • context.kind — should be tmux, zellij, herdr, or shell.
  • context.agent missing — the active pane does not contain a recognized coding agent.
  • context.agent.name present but context.agent.session missing — the process was recognized, but the gateway could not map it to a native session transcript. The agent icon may appear while Chat View remains unavailable.
  • servers: [] — no dev servers detected on this host.
  • error — session resolution error message from the daemon.

Debug Chat View

For a symptom-first walkthrough — no agent icon, icon but unavailable, opens but empty — start at Debugging Chat View. The sections below cover the gateway-side mechanics behind it.

Why does Chat View need an agent hook if it parses JSONL?

The transcript content does not come from hook events. The gateway reads the agent's local transcript — for Grok Build, its updates.jsonl — and sends the raw JSONL rows to the app, where the agent-specific parser turns them into messages and tool cards.

The hook supplies discovery metadata: the agent's native session ID, exact transcript path, and the terminal or multiplexer identifiers that own it. A process name such as grok or grok-build tells the gateway which agent is running, but not reliably which session UUID belongs to the visible pane. One process can have more than one recent or open session, so selecting the newest JSONL file could display the wrong conversation.

In short:

  • JSONL is the Chat View data source.
  • Hook state maps the visible terminal pane to the correct JSONL session.
  • /v1/transcripts streams that file directly from the host to the app.

Hook event summaries used for the inbox, notifications, and Live Activity are a separate path. Full transcript content never goes through the Moshi API.

Probe the transcript stream

Copy context.agent.name and context.agent.session from the /events response, then connect directly:

Grok Build transcript stream
$wscat -c "ws://127.0.0.1:24543/v1/transcripts?source=grok&session=<session-id>"
# Expected: a JSON message with type "backlog", source "grok", and entries

The same endpoint accepts the other Chat View source names. A healthy stream starts with backlog, then emits append as complete JSONL rows are written.

  • HTTP 404 / transcript not found — the session ID is valid, but its transcript cannot be resolved on disk.
  • HTTP 400 / invalid session id — the session value is malformed or belongs to a different source.
  • backlog arrives but the app does not render it — the host side is healthy; update the app and inspect its transcript parser.
  • The stream stops updating — confirm the agent is still writing the same session file. A new or resumed agent session gets a new stream.

Grok Build is detected but Chat View is unavailable

This specific state usually looks like agent=grok agentSession="" in the daemon log. Grok Build's process was detected, but no hook-written session mapping was found.

Check the installed binary and Grok Build hook:

Grok Build checks
$moshi-hook version
$moshi-hook status
$moshi-hook install --target grok

moshi-hook status should report Grok Build as current, with its managed config at ~/.grok/hooks/moshi-hooks.json. If it reports stale or not found, update moshi-hook, reinstall the Grok Build target, and restart the daemon. Submit another prompt after installing; if the running Grok Build process does not reload its hook config, restart Grok Build once.

After the next Grok Build hook event, reconnect to /events and confirm context.agent.session is populated. You can also verify the corresponding local transcript:

find the Grok Build JSONL
$find "${GROK_HOME:-$HOME/.grok}/sessions" -path "*/<session-id>/updates.jsonl" -print

Do not choose a different transcript merely because it has the newest modification time. The session ID returned for the active pane is the authority.

No updates after the first message?

The daemon only pushes when state changes. To force an update:

  • Switch tmux windows or panes.
  • cd to a different directory.
  • Start or stop a dev server.
  • Make a git commit or stage a file.

Common failures

context is null with no error

No session identifier was provided. Add ?session=ssh&sshConnection=... or ?session=mosh&... params.

error: "no matching sshd process"

The daemon can't find an sshd process matching the SSH_CONNECTION value:

inspect sshd
# On the host, list sshd processes:
$ps aux | grep sshd
# Verify the connection is still alive:
$ss -tnp | grep ssh

error: "no matching mosh-server"

The moshHost:moshPort doesn't match any running mosh-server:

inspect mosh
$lsof -i UDP | grep mosh

Common cause: connecting over Tailscale but the mosh-server bound to the LAN IP (or vice versa). The IPs must match exactly.

Gateway connects but iOS still shows "Diff unavailable"

The gateway works but the iOS app can't reach it. The app connects via SSH local port forward (not directly). Check:

  • Is the terminal session still alive in the iOS app?
  • Was the app reinstalled? (wipes saved connections)
  • Was brew services restart moshi-hook run after the terminal opened? (kills the forward target)

Reopen the terminal in the iOS app to re-establish the SSH tunnel.

Fedora/RHEL: SELinux resets the forwarded connection

If /v1/workspaces is healthy over loopback, but the app says the hook isn't available and Jump To fails, check ss. If there is no ESTAB connection to port 24543 from sshd, reproduce the app's tunnel path without the app:

probe the SSH tunnel
$ssh -N -L 24599:127.0.0.1:24543 localhost
# In another shell:
$curl -sS -o /dev/null -w "%{http_code}\n" http://127.0.0.1:24599/v1/diff/start
curl: (56) Recv failure: Connection reset by peer
000
$curl -sS -o /dev/null -w "%{http_code}\n" http://127.0.0.1:24543/v1/diff/start
405

On SELinux-enforcing Fedora or RHEL, this means SELinux may be blocking sshd_session_t from connecting to the gateway. This was seen after the selinux-policy 44.7 update. Confirm it with:

check SELinux denials
$ausearch -m avc | grep 24543

A name_connect denial from sshd-session confirms the tunnel backend is blocked. Relabeling the port with semanage port -a -t ssh_port_t -p tcp 24543 does not fix this path: in policy 44.7, sshd_session_t has no name_connect allow for that type, and its remaining unreserved/HTTP port grants depend on nis_enabled or authlogin_yubikey, which are off by default.

Install a minimal local policy instead:

module moshi_gateway 1.0;
require {
    type sshd_session_t;
    type unreserved_port_t;
    class tcp_socket name_connect;
}
allow sshd_session_t unreserved_port_t:tcp_socket name_connect;

Save it as m.te, then build and install it:

install the SELinux policy
$checkmodule -M -m -o m.mod m.te && semodule_package -o m.pp -m m.mod && semodule -i m.pp

The tunneled probe should now return 405, and Jump To should work with no app changes.

Daemon log

The daemon logs gateway events at DEBUG level:

tail the log
$tail -f ~/Library/Application\ Support/Moshi/hook.log | grep gateway

Key log lines:

  • gateway events: websocket connected — a client connected to /events.
  • gateway events: context request resolved — terminal lookup succeeded; when an agent is detected, this also shows agent and agentSession.
  • gateway events: context request failed — session lookup failed (shows error).
  • gateway transcripts: websocket connected — Chat View resolved the session transcript and opened its stream.
  • gateway transcripts: read range failed — the transcript was found but could not be read.
  • gateway events: server probe — dev server discovery results.