hitchrail 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- hitchrail-0.1.0/LICENSE +21 -0
- hitchrail-0.1.0/PKG-INFO +317 -0
- hitchrail-0.1.0/README.md +293 -0
- hitchrail-0.1.0/pyproject.toml +197 -0
- hitchrail-0.1.0/pyproject.toml.orig +261 -0
- hitchrail-0.1.0/src/hitchrail/__init__.py +10 -0
- hitchrail-0.1.0/src/hitchrail/claude_ipc.py +571 -0
- hitchrail-0.1.0/src/hitchrail/cli.py +326 -0
- hitchrail-0.1.0/src/hitchrail/config.py +468 -0
- hitchrail-0.1.0/src/hitchrail/derive.py +261 -0
- hitchrail-0.1.0/src/hitchrail/discovery.py +359 -0
- hitchrail-0.1.0/src/hitchrail/engine.py +785 -0
- hitchrail-0.1.0/src/hitchrail/events.py +150 -0
- hitchrail-0.1.0/src/hitchrail/headers.py +157 -0
- hitchrail-0.1.0/src/hitchrail/hostnames.py +239 -0
- hitchrail-0.1.0/src/hitchrail/pages.py +93 -0
- hitchrail-0.1.0/src/hitchrail/procs.py +202 -0
- hitchrail-0.1.0/src/hitchrail/projectnames.py +133 -0
- hitchrail-0.1.0/src/hitchrail/py.typed +0 -0
- hitchrail-0.1.0/src/hitchrail/ram.py +126 -0
- hitchrail-0.1.0/src/hitchrail/security.py +409 -0
- hitchrail-0.1.0/src/hitchrail/server.py +513 -0
- hitchrail-0.1.0/src/hitchrail/sessions.py +207 -0
- hitchrail-0.1.0/src/hitchrail/tmux.py +522 -0
- hitchrail-0.1.0/src/hitchrail/web/app.css +404 -0
- hitchrail-0.1.0/src/hitchrail/web/app.js +1382 -0
- hitchrail-0.1.0/src/hitchrail/web/fonts/IBMPlexMono-400.woff2 +0 -0
- hitchrail-0.1.0/src/hitchrail/web/fonts/Karla-400.woff2 +0 -0
- hitchrail-0.1.0/src/hitchrail/web/fonts/Karla-600.woff2 +0 -0
- hitchrail-0.1.0/src/hitchrail/web/fonts/Karla-700.woff2 +0 -0
- hitchrail-0.1.0/src/hitchrail/web/fonts/ZillaSlab-500.woff2 +0 -0
- hitchrail-0.1.0/src/hitchrail/web/fonts/ZillaSlab-700.woff2 +0 -0
- hitchrail-0.1.0/src/hitchrail/web/grant.html +229 -0
- hitchrail-0.1.0/src/hitchrail/web/index.html +61 -0
hitchrail-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 agigante80
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
hitchrail-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: hitchrail
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Start and stop headless Claude Code sessions across a folder of projects, from your phone.
|
|
5
|
+
Keywords: claude-code,tmux,session-manager,self-hosted
|
|
6
|
+
Author: agigante80
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Environment :: Web Environment
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Requires-Dist: starlette>=1.6,<2
|
|
18
|
+
Requires-Dist: uvicorn>=0.52,<1
|
|
19
|
+
Requires-Dist: sse-starlette>=3.4,<4
|
|
20
|
+
Requires-Python: >=3.11
|
|
21
|
+
Project-URL: Homepage, https://github.com/agigante80/hitchrail
|
|
22
|
+
Project-URL: Issues, https://github.com/agigante80/hitchrail/issues
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
# hitchrail
|
|
26
|
+
|
|
27
|
+
A web UI for starting and stopping headless Claude Code sessions across a
|
|
28
|
+
folder of projects. Open it on your phone, tap a folder, get a session link.
|
|
29
|
+
|
|
30
|
+
**Status: it runs.** Phases 0 to 6 are built and closed: the configuration and
|
|
31
|
+
its refusals, the folder discovery that makes the root a hard boundary, the
|
|
32
|
+
three security controls between a web page and a shell, the adapters, the
|
|
33
|
+
engine, the HTTP API and the browser interface. It has been driven from a real
|
|
34
|
+
phone against a real machine.
|
|
35
|
+
|
|
36
|
+
**It is not on PyPI yet**, so the `uvx` commands below do not work. Until they
|
|
37
|
+
do, run it from a clone: see [Run it](#run-it).
|
|
38
|
+
|
|
39
|
+
See [`docs/roadmap.md`](docs/roadmap.md) for what is left,
|
|
40
|
+
[`docs/superpowers/specs/2026-08-25-hitchrail-design.md`](docs/superpowers/specs/2026-08-25-hitchrail-design.md)
|
|
41
|
+
for the design, and [`docs/tech-guidelines.md`](docs/tech-guidelines.md) for the
|
|
42
|
+
engineering rules that govern the code.
|
|
43
|
+
|
|
44
|
+
## What it will do
|
|
45
|
+
|
|
46
|
+
Point it at a directory. It lists every folder inside, shows which ones have a
|
|
47
|
+
live Claude Code session, and lets you start or stop one with a single tap. It
|
|
48
|
+
shows memory pressure, refuses to start a session that would exhaust the
|
|
49
|
+
machine, and tails a session's output when you want to know what it is doing.
|
|
50
|
+
|
|
51
|
+
Stopping is a sequence rather than a button: it asks the agent to wrap up, shows
|
|
52
|
+
you the wait, and keeps a kill control within reach the whole time if you would
|
|
53
|
+
rather not wait.
|
|
54
|
+
|
|
55
|
+
## What it looks like
|
|
56
|
+
|
|
57
|
+
The phone case first, because it is the one this exists for.
|
|
58
|
+
|
|
59
|
+
| | |
|
|
60
|
+
|---|---|
|
|
61
|
+
| <img src="docs/screenshots/phone-list.png" alt="The project list on a phone: four folders showing running, stopped, detached with its pid, and stale" width="300"> | <img src="docs/screenshots/phone-list-dark.png" alt="The same list in the dark theme" width="300"> |
|
|
62
|
+
|
|
63
|
+
Four derived states in one listing: `running` with its memory and uptime,
|
|
64
|
+
`stopped`, `detached` with the pid of an agent that outlived its terminal, and
|
|
65
|
+
`stale` where a terminal outlived its agent.
|
|
66
|
+
|
|
67
|
+
<img src="docs/screenshots/desktop-list.png" alt="The same list at a desktop width" width="620">
|
|
68
|
+
|
|
69
|
+
These are captured from the running application against a scratch root, not
|
|
70
|
+
taken by hand: `uv run pytest -m screenshots` regenerates every one of them.
|
|
71
|
+
|
|
72
|
+
## What it costs you to run this
|
|
73
|
+
|
|
74
|
+
Hitchrail starts `claude --dangerously-skip-permissions`. **Anyone who can reach
|
|
75
|
+
its API can run arbitrary code on that machine as you.**
|
|
76
|
+
|
|
77
|
+
Every control below is built and tested, including on a real socket rather than
|
|
78
|
+
only in theory, and the API is now behind them. None of it is optional, and
|
|
79
|
+
none of it is a reason to run this on a network you do not trust.
|
|
80
|
+
|
|
81
|
+
The browser interface is built. The list, search and filtering, starting, the
|
|
82
|
+
stop sequence with its escalation, the log tail, creating a folder, the memory
|
|
83
|
+
footer, live updates over SSE with reconnection, the token screen and the dark
|
|
84
|
+
theme all work in a browser, and the end to end tier drives them. The warning
|
|
85
|
+
above applies to all of it exactly as written.
|
|
86
|
+
|
|
87
|
+
It binds to loopback with no authentication by default. Binding it to any other
|
|
88
|
+
interface requires a token, and the server refuses to start without one. It
|
|
89
|
+
validates the `Host` header on every request, because a localhost service
|
|
90
|
+
without that check can be driven by any website you visit, through DNS
|
|
91
|
+
rebinding. Over plain HTTP on a LAN the token crosses the network in cleartext;
|
|
92
|
+
put a TLS terminating reverse proxy in front of it if that matters to you.
|
|
93
|
+
|
|
94
|
+
Behind such a proxy, tell Hitchrail the origin the browser will actually send,
|
|
95
|
+
because it cannot be derived: the scheme and the port are the proxy's, not
|
|
96
|
+
ours.
|
|
97
|
+
|
|
98
|
+
```sh
|
|
99
|
+
hitchrail --root ~/dev --host 0.0.0.0 --allow-host box.lan \
|
|
100
|
+
--allow-origin https://box.lan
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
A trailing root dot makes no difference here: `box.lan` and `box.lan.` name the
|
|
104
|
+
same machine, so either spelling is accepted and either is matched. Browsers do
|
|
105
|
+
send the dotted form, because typing `http://box.lan./` is a way to force
|
|
106
|
+
absolute resolution on a split horizon network.
|
|
107
|
+
|
|
108
|
+
Getting the token onto a phone is a link rather than 32 characters of typing.
|
|
109
|
+
Open `http://<address>:8787/grant#token=<token>` once. The token is everything
|
|
110
|
+
after the `#`, and a fragment is never sent to a server: not to Hitchrail, not
|
|
111
|
+
to a reverse proxy, and not in a `Referer` header. The page reads it in the
|
|
112
|
+
browser, trades it for a cookie, and clears the address bar.
|
|
113
|
+
|
|
114
|
+
`hitchrail` prints that link for every address it can be reached on, so it is
|
|
115
|
+
copied rather than typed.
|
|
116
|
+
|
|
117
|
+
Treat the link as a secret anyway, because it is one, and the phone it lands on
|
|
118
|
+
is where it now lives. What changed is the set of machines that write it down.
|
|
119
|
+
Every server side one is gone: this server's access log, any proxy in front,
|
|
120
|
+
and the `Referer` header on anything the page fetches.
|
|
121
|
+
|
|
122
|
+
The browser is narrowed rather than cleared, and the difference is worth
|
|
123
|
+
stating rather than rounding off. The page rewrites its own history entry, so
|
|
124
|
+
the entry does not keep the key. Pasting the link into the address bar is
|
|
125
|
+
another matter: that can leave a typed URL in autocomplete, and autocomplete
|
|
126
|
+
syncs. Open the link by tapping it rather than by pasting it, and the
|
|
127
|
+
distinction does not arise.
|
|
128
|
+
|
|
129
|
+
The older `?token=<token>` form is gone. It is a query parameter now, not a
|
|
130
|
+
credential: a request carrying one is refused like any other request with no
|
|
131
|
+
token, and it appears in the server's log like any other query string.
|
|
132
|
+
|
|
133
|
+
Hitchrail does not sandbox the sessions it starts. It is a launcher. The agent it
|
|
134
|
+
launches has whatever access you have.
|
|
135
|
+
|
|
136
|
+
**Whoever holds the token can cause characters to be typed into any agent
|
|
137
|
+
session under your root.** Stopping an agent works by sending it keystrokes
|
|
138
|
+
through its terminal, and an agent reading its own input cannot tell those from
|
|
139
|
+
you typing. That is what makes a gentle stop possible at all, and it is worth
|
|
140
|
+
reading rather than discovering. Hitchrail only ever sends the stop sequence,
|
|
141
|
+
and one test enforces that only the module owning it may send anything.
|
|
142
|
+
|
|
143
|
+
Hitchrail cannot end a `detached` agent, the state where a process outlived its
|
|
144
|
+
terminal. It shows the pid and stops there, because everything it can destroy
|
|
145
|
+
is addressed by the session name it created, and signalling a bare pid would be
|
|
146
|
+
the first thing outside that.
|
|
147
|
+
|
|
148
|
+
**Found a hole?** [`SECURITY.md`](SECURITY.md) says what is in scope, what is
|
|
149
|
+
this design rather than a bug, and where to report privately. Please do not
|
|
150
|
+
open a public issue for a vulnerability.
|
|
151
|
+
|
|
152
|
+
## Prerequisites
|
|
153
|
+
|
|
154
|
+
Hitchrail is a launcher, so the things it launches have to already be there. It
|
|
155
|
+
does not vendor or install any of them.
|
|
156
|
+
|
|
157
|
+
| Needed | Why | Checked |
|
|
158
|
+
|---|---|---|
|
|
159
|
+
| **tmux** | every session Hitchrail starts lives in a tmux session; this is the whole mechanism, not an option | `tmux -V` |
|
|
160
|
+
| **Claude Code on `PATH`** | it is what Hitchrail runs. Configurable with `--agent-binary` | `claude --version` |
|
|
161
|
+
| **Linux** | memory pressure is read from `/proc/meminfo`, and the process table from `ps`. macOS has neither in this form, which is why the package declares `Operating System :: POSIX :: Linux` | |
|
|
162
|
+
| **Python 3.11+** | `uvx` and `pipx` handle this for you | `python3 --version` |
|
|
163
|
+
|
|
164
|
+
Installing Hitchrail with `uvx` will succeed on a machine with no tmux and no
|
|
165
|
+
Claude Code, because neither is a Python dependency. It will then fail at the
|
|
166
|
+
first attempt to start a session. Check the two commands above first.
|
|
167
|
+
|
|
168
|
+
## Run it
|
|
169
|
+
|
|
170
|
+
Nothing is published yet, so this is the way in. It needs `uv`, `tmux` and
|
|
171
|
+
Claude Code on `PATH`, per the table above.
|
|
172
|
+
|
|
173
|
+
```sh
|
|
174
|
+
git clone https://github.com/agigante80/hitchrail
|
|
175
|
+
cd hitchrail
|
|
176
|
+
uv run hitchrail --root ~/projects
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
`--root` is the folder holding your projects. Every directory directly inside it
|
|
180
|
+
becomes a row. **Point it at a scratch folder the first time**: Hitchrail only
|
|
181
|
+
recognises the tmux sessions it started itself, so starting a project that
|
|
182
|
+
already has a session from another tool gives you a second agent in the same
|
|
183
|
+
directory.
|
|
184
|
+
|
|
185
|
+
On loopback that is all.
|
|
186
|
+
|
|
187
|
+
To open it from your phone you have to decide how the phone reaches it, and
|
|
188
|
+
there are three answers with an order to them. **[`docs/guides/phone-access.md`](docs/guides/phone-access.md)
|
|
189
|
+
is that decision**, best first: an overlay network such as `tailscale serve`,
|
|
190
|
+
which needs no inbound port and survives the machine changing networks; a named
|
|
191
|
+
LAN address; and never the wildcard. The default is loopback, so the safe thing
|
|
192
|
+
is what happens when you pass nothing.
|
|
193
|
+
|
|
194
|
+
The middle route is the one with no prerequisites, and it is the one shown
|
|
195
|
+
here. Bind to the machine's LAN address and it prints a link to tap:
|
|
196
|
+
|
|
197
|
+
```sh
|
|
198
|
+
uv run hitchrail --root ~/projects --host 192.168.1.10
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
```
|
|
202
|
+
token: <generated>
|
|
203
|
+
Anyone with this token can run code on this machine as you.
|
|
204
|
+
|
|
205
|
+
Open one of these on your phone:
|
|
206
|
+
http://192.168.1.10:8787/grant#token=<generated>
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
A token is generated and REQUIRED as soon as anything outside this machine can
|
|
210
|
+
reach Hitchrail. Binding off loopback is one way to say so; passing
|
|
211
|
+
`--allow-host` or `--allow-origin` for a name that is not loopback is the other,
|
|
212
|
+
because that is what you do to put Hitchrail behind a proxy such as
|
|
213
|
+
`tailscale serve`. In both cases the server refuses to start without one. Everything after the `#` stays in the browser and
|
|
214
|
+
reaches no server log. Over plain HTTP the cookie it becomes still crosses your
|
|
215
|
+
network in clear, so put TLS in front of it if that matters to you.
|
|
216
|
+
|
|
217
|
+
### Where the token comes from
|
|
218
|
+
|
|
219
|
+
In order: `--token`, then `HITCHRAIL_TOKEN` in the environment, then one
|
|
220
|
+
generated for you and printed.
|
|
221
|
+
|
|
222
|
+
**Prefer the environment variable to the flag on any machine you share.** On
|
|
223
|
+
Linux `/proc/<pid>/cmdline` is world readable and `/proc/<pid>/environ` is not,
|
|
224
|
+
so `--token` shows your token to every other account on the box, and `ps` does
|
|
225
|
+
it for them without their having to try. The environment is readable only by
|
|
226
|
+
you and root.
|
|
227
|
+
|
|
228
|
+
`HITCHRAIL_TOKEN` set but empty is refused rather than treated as absent. An
|
|
229
|
+
operator who writes it into a file and leaves the value off has not configured
|
|
230
|
+
authentication, and Hitchrail says so instead of quietly generating one.
|
|
231
|
+
|
|
232
|
+
It is also what makes a long running Hitchrail usable: a generated token
|
|
233
|
+
changes on every start, so a service that restarts invalidates the link saved
|
|
234
|
+
on your phone. A token from the environment survives.
|
|
235
|
+
|
|
236
|
+
### Keeping it running
|
|
237
|
+
|
|
238
|
+
Hitchrail dies when you close the terminal, and a phone is useful precisely
|
|
239
|
+
when you are not at the machine. `packaging/hitchrail.service` is a systemd
|
|
240
|
+
**user** unit template: copy it, edit the paths, and it survives logout and
|
|
241
|
+
reboot.
|
|
242
|
+
|
|
243
|
+
```sh
|
|
244
|
+
cp packaging/hitchrail.service ~/.config/systemd/user/
|
|
245
|
+
systemctl --user enable --now hitchrail
|
|
246
|
+
loginctl enable-linger "$USER"
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
A user unit rather than a system one, because Hitchrail spawns agents as you
|
|
250
|
+
and a `User=` invites running a shell as root. `Restart=on-failure` rather than
|
|
251
|
+
`always`, because a configuration Hitchrail refuses is a deliberate stop and a
|
|
252
|
+
boot loop would bury the reason. The token must come from the unit's
|
|
253
|
+
`EnvironmentFile` at mode 600: a generated one changes every restart and kills
|
|
254
|
+
the link on your phone, and the banner withholds it from the journal anyway.
|
|
255
|
+
|
|
256
|
+
**An always on service is a standing exposure rather than a session shaped
|
|
257
|
+
one.** Until now the window in which this was reachable was the window in which
|
|
258
|
+
you were watching it. Read [`docs/guides/phone-access.md`](docs/guides/phone-access.md)
|
|
259
|
+
before enabling it.
|
|
260
|
+
|
|
261
|
+
## Install
|
|
262
|
+
|
|
263
|
+
**Not yet.** `hitchrail` is not on PyPI, so none of these work today. They are
|
|
264
|
+
here so the intended shape is on the record, and they become true at Phase 8.
|
|
265
|
+
|
|
266
|
+
Hitchrail is a Python package, so the equivalent of `npx` here is `uvx`:
|
|
267
|
+
|
|
268
|
+
```sh
|
|
269
|
+
uvx hitchrail # run it, install nothing
|
|
270
|
+
uv tool install hitchrail # keep it on PATH
|
|
271
|
+
pipx install hitchrail # if you already live in pipx
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
One word, no hyphen.
|
|
275
|
+
|
|
276
|
+
**The service route is `uv tool install`, not `uvx`.** `uvx` resolves and runs
|
|
277
|
+
out of a cache it is free to evict, which is what makes it good for trying
|
|
278
|
+
something and wrong for a unit: the systemd unit needs an executable path that
|
|
279
|
+
is still there next month. That is why the template's `ExecStart` names
|
|
280
|
+
`~/.local/bin/hitchrail`.
|
|
281
|
+
|
|
282
|
+
## Working on it
|
|
283
|
+
|
|
284
|
+
```sh
|
|
285
|
+
uv sync # set up
|
|
286
|
+
uv run pytest # tests
|
|
287
|
+
uv run ruff check # lint
|
|
288
|
+
uv run ruff format # format
|
|
289
|
+
uv run mypy # types
|
|
290
|
+
uv run lint-imports # module boundaries
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
All five are blocking in CI on 3.11, 3.12 and 3.13. The last one is the
|
|
294
|
+
unusual one: it enforces that the engine layer never imports Starlette,
|
|
295
|
+
uvicorn, `sse_starlette`, the server or the CLI, so the engine stays testable
|
|
296
|
+
without HTTP. Import boundaries defended only by good intentions do not
|
|
297
|
+
survive.
|
|
298
|
+
|
|
299
|
+
## Documents
|
|
300
|
+
|
|
301
|
+
| | |
|
|
302
|
+
|---|---|
|
|
303
|
+
| [`docs/api.md`](docs/api.md) | the HTTP API: routes, auth, and every error code |
|
|
304
|
+
| [`SECURITY.md`](SECURITY.md) | what is in scope, and where to report it privately |
|
|
305
|
+
| [`CONTRIBUTING.md`](CONTRIBUTING.md) | how a change is expected to arrive |
|
|
306
|
+
| [`CHANGELOG.md`](CHANGELOG.md) | what upgrading costs you |
|
|
307
|
+
| [`docs/releasing.md`](docs/releasing.md) | how a release is cut and published |
|
|
308
|
+
| [`AGENTS.md`](AGENTS.md) | the architecture and the non negotiables |
|
|
309
|
+
|
|
310
|
+
## Not affiliated with Anthropic
|
|
311
|
+
|
|
312
|
+
Hitchrail is an independent open source tool. Claude and Claude Code are
|
|
313
|
+
trademarks of Anthropic.
|
|
314
|
+
|
|
315
|
+
## Licence
|
|
316
|
+
|
|
317
|
+
MIT.
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
# hitchrail
|
|
2
|
+
|
|
3
|
+
A web UI for starting and stopping headless Claude Code sessions across a
|
|
4
|
+
folder of projects. Open it on your phone, tap a folder, get a session link.
|
|
5
|
+
|
|
6
|
+
**Status: it runs.** Phases 0 to 6 are built and closed: the configuration and
|
|
7
|
+
its refusals, the folder discovery that makes the root a hard boundary, the
|
|
8
|
+
three security controls between a web page and a shell, the adapters, the
|
|
9
|
+
engine, the HTTP API and the browser interface. It has been driven from a real
|
|
10
|
+
phone against a real machine.
|
|
11
|
+
|
|
12
|
+
**It is not on PyPI yet**, so the `uvx` commands below do not work. Until they
|
|
13
|
+
do, run it from a clone: see [Run it](#run-it).
|
|
14
|
+
|
|
15
|
+
See [`docs/roadmap.md`](docs/roadmap.md) for what is left,
|
|
16
|
+
[`docs/superpowers/specs/2026-08-25-hitchrail-design.md`](docs/superpowers/specs/2026-08-25-hitchrail-design.md)
|
|
17
|
+
for the design, and [`docs/tech-guidelines.md`](docs/tech-guidelines.md) for the
|
|
18
|
+
engineering rules that govern the code.
|
|
19
|
+
|
|
20
|
+
## What it will do
|
|
21
|
+
|
|
22
|
+
Point it at a directory. It lists every folder inside, shows which ones have a
|
|
23
|
+
live Claude Code session, and lets you start or stop one with a single tap. It
|
|
24
|
+
shows memory pressure, refuses to start a session that would exhaust the
|
|
25
|
+
machine, and tails a session's output when you want to know what it is doing.
|
|
26
|
+
|
|
27
|
+
Stopping is a sequence rather than a button: it asks the agent to wrap up, shows
|
|
28
|
+
you the wait, and keeps a kill control within reach the whole time if you would
|
|
29
|
+
rather not wait.
|
|
30
|
+
|
|
31
|
+
## What it looks like
|
|
32
|
+
|
|
33
|
+
The phone case first, because it is the one this exists for.
|
|
34
|
+
|
|
35
|
+
| | |
|
|
36
|
+
|---|---|
|
|
37
|
+
| <img src="docs/screenshots/phone-list.png" alt="The project list on a phone: four folders showing running, stopped, detached with its pid, and stale" width="300"> | <img src="docs/screenshots/phone-list-dark.png" alt="The same list in the dark theme" width="300"> |
|
|
38
|
+
|
|
39
|
+
Four derived states in one listing: `running` with its memory and uptime,
|
|
40
|
+
`stopped`, `detached` with the pid of an agent that outlived its terminal, and
|
|
41
|
+
`stale` where a terminal outlived its agent.
|
|
42
|
+
|
|
43
|
+
<img src="docs/screenshots/desktop-list.png" alt="The same list at a desktop width" width="620">
|
|
44
|
+
|
|
45
|
+
These are captured from the running application against a scratch root, not
|
|
46
|
+
taken by hand: `uv run pytest -m screenshots` regenerates every one of them.
|
|
47
|
+
|
|
48
|
+
## What it costs you to run this
|
|
49
|
+
|
|
50
|
+
Hitchrail starts `claude --dangerously-skip-permissions`. **Anyone who can reach
|
|
51
|
+
its API can run arbitrary code on that machine as you.**
|
|
52
|
+
|
|
53
|
+
Every control below is built and tested, including on a real socket rather than
|
|
54
|
+
only in theory, and the API is now behind them. None of it is optional, and
|
|
55
|
+
none of it is a reason to run this on a network you do not trust.
|
|
56
|
+
|
|
57
|
+
The browser interface is built. The list, search and filtering, starting, the
|
|
58
|
+
stop sequence with its escalation, the log tail, creating a folder, the memory
|
|
59
|
+
footer, live updates over SSE with reconnection, the token screen and the dark
|
|
60
|
+
theme all work in a browser, and the end to end tier drives them. The warning
|
|
61
|
+
above applies to all of it exactly as written.
|
|
62
|
+
|
|
63
|
+
It binds to loopback with no authentication by default. Binding it to any other
|
|
64
|
+
interface requires a token, and the server refuses to start without one. It
|
|
65
|
+
validates the `Host` header on every request, because a localhost service
|
|
66
|
+
without that check can be driven by any website you visit, through DNS
|
|
67
|
+
rebinding. Over plain HTTP on a LAN the token crosses the network in cleartext;
|
|
68
|
+
put a TLS terminating reverse proxy in front of it if that matters to you.
|
|
69
|
+
|
|
70
|
+
Behind such a proxy, tell Hitchrail the origin the browser will actually send,
|
|
71
|
+
because it cannot be derived: the scheme and the port are the proxy's, not
|
|
72
|
+
ours.
|
|
73
|
+
|
|
74
|
+
```sh
|
|
75
|
+
hitchrail --root ~/dev --host 0.0.0.0 --allow-host box.lan \
|
|
76
|
+
--allow-origin https://box.lan
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
A trailing root dot makes no difference here: `box.lan` and `box.lan.` name the
|
|
80
|
+
same machine, so either spelling is accepted and either is matched. Browsers do
|
|
81
|
+
send the dotted form, because typing `http://box.lan./` is a way to force
|
|
82
|
+
absolute resolution on a split horizon network.
|
|
83
|
+
|
|
84
|
+
Getting the token onto a phone is a link rather than 32 characters of typing.
|
|
85
|
+
Open `http://<address>:8787/grant#token=<token>` once. The token is everything
|
|
86
|
+
after the `#`, and a fragment is never sent to a server: not to Hitchrail, not
|
|
87
|
+
to a reverse proxy, and not in a `Referer` header. The page reads it in the
|
|
88
|
+
browser, trades it for a cookie, and clears the address bar.
|
|
89
|
+
|
|
90
|
+
`hitchrail` prints that link for every address it can be reached on, so it is
|
|
91
|
+
copied rather than typed.
|
|
92
|
+
|
|
93
|
+
Treat the link as a secret anyway, because it is one, and the phone it lands on
|
|
94
|
+
is where it now lives. What changed is the set of machines that write it down.
|
|
95
|
+
Every server side one is gone: this server's access log, any proxy in front,
|
|
96
|
+
and the `Referer` header on anything the page fetches.
|
|
97
|
+
|
|
98
|
+
The browser is narrowed rather than cleared, and the difference is worth
|
|
99
|
+
stating rather than rounding off. The page rewrites its own history entry, so
|
|
100
|
+
the entry does not keep the key. Pasting the link into the address bar is
|
|
101
|
+
another matter: that can leave a typed URL in autocomplete, and autocomplete
|
|
102
|
+
syncs. Open the link by tapping it rather than by pasting it, and the
|
|
103
|
+
distinction does not arise.
|
|
104
|
+
|
|
105
|
+
The older `?token=<token>` form is gone. It is a query parameter now, not a
|
|
106
|
+
credential: a request carrying one is refused like any other request with no
|
|
107
|
+
token, and it appears in the server's log like any other query string.
|
|
108
|
+
|
|
109
|
+
Hitchrail does not sandbox the sessions it starts. It is a launcher. The agent it
|
|
110
|
+
launches has whatever access you have.
|
|
111
|
+
|
|
112
|
+
**Whoever holds the token can cause characters to be typed into any agent
|
|
113
|
+
session under your root.** Stopping an agent works by sending it keystrokes
|
|
114
|
+
through its terminal, and an agent reading its own input cannot tell those from
|
|
115
|
+
you typing. That is what makes a gentle stop possible at all, and it is worth
|
|
116
|
+
reading rather than discovering. Hitchrail only ever sends the stop sequence,
|
|
117
|
+
and one test enforces that only the module owning it may send anything.
|
|
118
|
+
|
|
119
|
+
Hitchrail cannot end a `detached` agent, the state where a process outlived its
|
|
120
|
+
terminal. It shows the pid and stops there, because everything it can destroy
|
|
121
|
+
is addressed by the session name it created, and signalling a bare pid would be
|
|
122
|
+
the first thing outside that.
|
|
123
|
+
|
|
124
|
+
**Found a hole?** [`SECURITY.md`](SECURITY.md) says what is in scope, what is
|
|
125
|
+
this design rather than a bug, and where to report privately. Please do not
|
|
126
|
+
open a public issue for a vulnerability.
|
|
127
|
+
|
|
128
|
+
## Prerequisites
|
|
129
|
+
|
|
130
|
+
Hitchrail is a launcher, so the things it launches have to already be there. It
|
|
131
|
+
does not vendor or install any of them.
|
|
132
|
+
|
|
133
|
+
| Needed | Why | Checked |
|
|
134
|
+
|---|---|---|
|
|
135
|
+
| **tmux** | every session Hitchrail starts lives in a tmux session; this is the whole mechanism, not an option | `tmux -V` |
|
|
136
|
+
| **Claude Code on `PATH`** | it is what Hitchrail runs. Configurable with `--agent-binary` | `claude --version` |
|
|
137
|
+
| **Linux** | memory pressure is read from `/proc/meminfo`, and the process table from `ps`. macOS has neither in this form, which is why the package declares `Operating System :: POSIX :: Linux` | |
|
|
138
|
+
| **Python 3.11+** | `uvx` and `pipx` handle this for you | `python3 --version` |
|
|
139
|
+
|
|
140
|
+
Installing Hitchrail with `uvx` will succeed on a machine with no tmux and no
|
|
141
|
+
Claude Code, because neither is a Python dependency. It will then fail at the
|
|
142
|
+
first attempt to start a session. Check the two commands above first.
|
|
143
|
+
|
|
144
|
+
## Run it
|
|
145
|
+
|
|
146
|
+
Nothing is published yet, so this is the way in. It needs `uv`, `tmux` and
|
|
147
|
+
Claude Code on `PATH`, per the table above.
|
|
148
|
+
|
|
149
|
+
```sh
|
|
150
|
+
git clone https://github.com/agigante80/hitchrail
|
|
151
|
+
cd hitchrail
|
|
152
|
+
uv run hitchrail --root ~/projects
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
`--root` is the folder holding your projects. Every directory directly inside it
|
|
156
|
+
becomes a row. **Point it at a scratch folder the first time**: Hitchrail only
|
|
157
|
+
recognises the tmux sessions it started itself, so starting a project that
|
|
158
|
+
already has a session from another tool gives you a second agent in the same
|
|
159
|
+
directory.
|
|
160
|
+
|
|
161
|
+
On loopback that is all.
|
|
162
|
+
|
|
163
|
+
To open it from your phone you have to decide how the phone reaches it, and
|
|
164
|
+
there are three answers with an order to them. **[`docs/guides/phone-access.md`](docs/guides/phone-access.md)
|
|
165
|
+
is that decision**, best first: an overlay network such as `tailscale serve`,
|
|
166
|
+
which needs no inbound port and survives the machine changing networks; a named
|
|
167
|
+
LAN address; and never the wildcard. The default is loopback, so the safe thing
|
|
168
|
+
is what happens when you pass nothing.
|
|
169
|
+
|
|
170
|
+
The middle route is the one with no prerequisites, and it is the one shown
|
|
171
|
+
here. Bind to the machine's LAN address and it prints a link to tap:
|
|
172
|
+
|
|
173
|
+
```sh
|
|
174
|
+
uv run hitchrail --root ~/projects --host 192.168.1.10
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
```
|
|
178
|
+
token: <generated>
|
|
179
|
+
Anyone with this token can run code on this machine as you.
|
|
180
|
+
|
|
181
|
+
Open one of these on your phone:
|
|
182
|
+
http://192.168.1.10:8787/grant#token=<generated>
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
A token is generated and REQUIRED as soon as anything outside this machine can
|
|
186
|
+
reach Hitchrail. Binding off loopback is one way to say so; passing
|
|
187
|
+
`--allow-host` or `--allow-origin` for a name that is not loopback is the other,
|
|
188
|
+
because that is what you do to put Hitchrail behind a proxy such as
|
|
189
|
+
`tailscale serve`. In both cases the server refuses to start without one. Everything after the `#` stays in the browser and
|
|
190
|
+
reaches no server log. Over plain HTTP the cookie it becomes still crosses your
|
|
191
|
+
network in clear, so put TLS in front of it if that matters to you.
|
|
192
|
+
|
|
193
|
+
### Where the token comes from
|
|
194
|
+
|
|
195
|
+
In order: `--token`, then `HITCHRAIL_TOKEN` in the environment, then one
|
|
196
|
+
generated for you and printed.
|
|
197
|
+
|
|
198
|
+
**Prefer the environment variable to the flag on any machine you share.** On
|
|
199
|
+
Linux `/proc/<pid>/cmdline` is world readable and `/proc/<pid>/environ` is not,
|
|
200
|
+
so `--token` shows your token to every other account on the box, and `ps` does
|
|
201
|
+
it for them without their having to try. The environment is readable only by
|
|
202
|
+
you and root.
|
|
203
|
+
|
|
204
|
+
`HITCHRAIL_TOKEN` set but empty is refused rather than treated as absent. An
|
|
205
|
+
operator who writes it into a file and leaves the value off has not configured
|
|
206
|
+
authentication, and Hitchrail says so instead of quietly generating one.
|
|
207
|
+
|
|
208
|
+
It is also what makes a long running Hitchrail usable: a generated token
|
|
209
|
+
changes on every start, so a service that restarts invalidates the link saved
|
|
210
|
+
on your phone. A token from the environment survives.
|
|
211
|
+
|
|
212
|
+
### Keeping it running
|
|
213
|
+
|
|
214
|
+
Hitchrail dies when you close the terminal, and a phone is useful precisely
|
|
215
|
+
when you are not at the machine. `packaging/hitchrail.service` is a systemd
|
|
216
|
+
**user** unit template: copy it, edit the paths, and it survives logout and
|
|
217
|
+
reboot.
|
|
218
|
+
|
|
219
|
+
```sh
|
|
220
|
+
cp packaging/hitchrail.service ~/.config/systemd/user/
|
|
221
|
+
systemctl --user enable --now hitchrail
|
|
222
|
+
loginctl enable-linger "$USER"
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
A user unit rather than a system one, because Hitchrail spawns agents as you
|
|
226
|
+
and a `User=` invites running a shell as root. `Restart=on-failure` rather than
|
|
227
|
+
`always`, because a configuration Hitchrail refuses is a deliberate stop and a
|
|
228
|
+
boot loop would bury the reason. The token must come from the unit's
|
|
229
|
+
`EnvironmentFile` at mode 600: a generated one changes every restart and kills
|
|
230
|
+
the link on your phone, and the banner withholds it from the journal anyway.
|
|
231
|
+
|
|
232
|
+
**An always on service is a standing exposure rather than a session shaped
|
|
233
|
+
one.** Until now the window in which this was reachable was the window in which
|
|
234
|
+
you were watching it. Read [`docs/guides/phone-access.md`](docs/guides/phone-access.md)
|
|
235
|
+
before enabling it.
|
|
236
|
+
|
|
237
|
+
## Install
|
|
238
|
+
|
|
239
|
+
**Not yet.** `hitchrail` is not on PyPI, so none of these work today. They are
|
|
240
|
+
here so the intended shape is on the record, and they become true at Phase 8.
|
|
241
|
+
|
|
242
|
+
Hitchrail is a Python package, so the equivalent of `npx` here is `uvx`:
|
|
243
|
+
|
|
244
|
+
```sh
|
|
245
|
+
uvx hitchrail # run it, install nothing
|
|
246
|
+
uv tool install hitchrail # keep it on PATH
|
|
247
|
+
pipx install hitchrail # if you already live in pipx
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
One word, no hyphen.
|
|
251
|
+
|
|
252
|
+
**The service route is `uv tool install`, not `uvx`.** `uvx` resolves and runs
|
|
253
|
+
out of a cache it is free to evict, which is what makes it good for trying
|
|
254
|
+
something and wrong for a unit: the systemd unit needs an executable path that
|
|
255
|
+
is still there next month. That is why the template's `ExecStart` names
|
|
256
|
+
`~/.local/bin/hitchrail`.
|
|
257
|
+
|
|
258
|
+
## Working on it
|
|
259
|
+
|
|
260
|
+
```sh
|
|
261
|
+
uv sync # set up
|
|
262
|
+
uv run pytest # tests
|
|
263
|
+
uv run ruff check # lint
|
|
264
|
+
uv run ruff format # format
|
|
265
|
+
uv run mypy # types
|
|
266
|
+
uv run lint-imports # module boundaries
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
All five are blocking in CI on 3.11, 3.12 and 3.13. The last one is the
|
|
270
|
+
unusual one: it enforces that the engine layer never imports Starlette,
|
|
271
|
+
uvicorn, `sse_starlette`, the server or the CLI, so the engine stays testable
|
|
272
|
+
without HTTP. Import boundaries defended only by good intentions do not
|
|
273
|
+
survive.
|
|
274
|
+
|
|
275
|
+
## Documents
|
|
276
|
+
|
|
277
|
+
| | |
|
|
278
|
+
|---|---|
|
|
279
|
+
| [`docs/api.md`](docs/api.md) | the HTTP API: routes, auth, and every error code |
|
|
280
|
+
| [`SECURITY.md`](SECURITY.md) | what is in scope, and where to report it privately |
|
|
281
|
+
| [`CONTRIBUTING.md`](CONTRIBUTING.md) | how a change is expected to arrive |
|
|
282
|
+
| [`CHANGELOG.md`](CHANGELOG.md) | what upgrading costs you |
|
|
283
|
+
| [`docs/releasing.md`](docs/releasing.md) | how a release is cut and published |
|
|
284
|
+
| [`AGENTS.md`](AGENTS.md) | the architecture and the non negotiables |
|
|
285
|
+
|
|
286
|
+
## Not affiliated with Anthropic
|
|
287
|
+
|
|
288
|
+
Hitchrail is an independent open source tool. Claude and Claude Code are
|
|
289
|
+
trademarks of Anthropic.
|
|
290
|
+
|
|
291
|
+
## Licence
|
|
292
|
+
|
|
293
|
+
MIT.
|