# HarborSSH

**Your hosts. Your keys. Your approval.**

A self-hosted, single-owner SSH connector for ChatGPT plugins. Configure an SSH
host once, connect the authenticated MCP endpoint in ChatGPT, and ask for work
using a memorable host alias. Review exact shell commands in a separate local
dashboard; inspect real stdout, stderr and exit status from the conversation.

This release contains the complete TypeScript implementation, compiled JavaScript,
a local web dashboard, a portable plugin manifest and skill, automated tests, and
deployment examples. **No runtime npm packages or OpenAI API key are required.**
It is a personal connector implementation, not an already-published directory
listing, a hosted service, or an independently audited security product.

## How it connects

```text
ChatGPT plugin
  │  HTTPS / MCP Streamable HTTP / OAuth + PKCE
  ▼
Your HTTPS reverse proxy or tunnel
  │  public origin → 127.0.0.1:8787 ONLY
  ▼
HarborSSH bridge ──────────────► installed OpenSSH ──► authorized SSH account
  ▲                                                  local or remote host
  │  separate local approval
127.0.0.1:8788
Local dashboard, protected by a local-only administrator secret
```

**The SSH account must not be able to read the bridge account's state, private
keys, source code, or agent socket.** In particular, using unrestricted SSH into
the same operating-system account that runs the bridge defeats the local-approval
boundary: that account could read the administrator secret. Use a separate
restricted SSH user, VM, or container with appropriate filesystem/network policy.
The configured working directory is **not** a sandbox.

## Start here

Prerequisites: Node.js 22 or newer, installed OpenSSH client tools (`ssh` and
`ssh-keyscan`), and an authorized SSH server accepting a dedicated public key.
This version executes **non-interactive POSIX shell commands**; Linux/macOS/WSL
SSH targets fit that contract. Native Windows PowerShell SSH targets do not.
Windows bridge support is unverified and requires explicit NTFS ACL review;
Linux/macOS are the intended initial deployment environments.

Extract this distribution and use the included `dist/` directly. A Unix
`./harborssh` launcher and Windows `harborssh.cmd` launcher are also included:

```sh
cd HarborSSH
node dist/cli.js init
```

The wizard asks for your public HTTPS origin and an SSH profile: alias, host,
account, working directory, local identity file or SSH agent, and a verified host
fingerprint. It creates private state in `~/.harborssh`. Supply the public origin
you will expose, such as `https://ssh.example.com`; the default loopback URL is
only suitable for local testing and is not reachable by ChatGPT web.

Obtain the host fingerprint through an independently trusted server console:

```sh
ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub
```

Do not blindly accept a fingerprint obtained from the same unauthenticated scan.
Alternatively, `--host-key-file /trusted/ssh_host_ed25519_key.pub` accepts a
trusted public host-key file without scanning. This is a **public host key**, not
the client's private authentication key.

For repeatable non-interactive configuration:

```sh
node dist/cli.js init --public-url https://ssh.example.com --no-host

node dist/cli.js host add \
  --alias dev \
  --host 127.0.0.1 \
  --port 22 \
  --user chatgpt-runner \
  --key "$HOME/.ssh/harborssh_runner_ed25519" \
  --cwd /home/chatgpt-runner \
  --host-key-file /trusted/ssh_host_ed25519_key.pub

node dist/cli.js host test dev
node dist/cli.js start
```

Replace the example username, key path, working directory and trusted public-key
path with your actual provisioned values. `127.0.0.1` targets the bridge machine's
SSH server; replace it with a LAN/remote hostname to operate another machine.
`host test` is a local owner command and executes `id && pwd && uname -a` directly.

In another terminal, open the approval dashboard:

```sh
cd HarborSSH
node dist/cli.js ui
```

The CLI opens a private local URL and the dashboard clears its secret fragment
from the address bar. `ui --print` is provided for headless troubleshooting;
never paste that URL or its token into ChatGPT.

## Expose the bridge, not the administrator

Route your public HTTPS origin to **`http://127.0.0.1:8787`**, including `/mcp`,
OAuth, and discovery paths. Do **not** publish port 8788. A stable named tunnel
works for a development machine behind NAT; a conventional reverse proxy works
on a reachable server. Both approaches are documented in
[Deployment](docs/DEPLOYMENT.md), with Caddy and Cloudflare examples in `deploy/`.

The SSH port itself need not be exposed publicly when the bridge reaches the SSH
server locally. This package does not create a domain, tunnel account, or SSH
server for you, and does not silently open a listening port on every interface.

## Connect in ChatGPT

The current OpenAI plugin quickstart uses this flow, checked September 13, 2026:

1. Enable **Settings → Security and login → Developer mode**.
2. Open **https://chatgpt.com/plugins**, select **+**, and provide
   **`https://YOUR-HOST/mcp`**. Select OAuth when asked; the server supports dynamic
   client registration, so do not invent a client ID or client secret.
3. On the HarborSSH consent page, enter a one-time connection code generated in
   the local dashboard. Check the page's domain, client details and callback.
   The code belongs on that page, **not in chat**.
4. Install the resulting personal plugin. Open a new **Work** chat, type **@**, and
   select **HarborSSH**.

Account/workspace permissions can affect whether developer mode is available.
Detailed callback handling and troubleshooting are in
[ChatGPT connection](docs/CHATGPT.md). Personal developer registration and public
plugin-directory publication are different operations; this archive performs
neither on your behalf.

Try these requests:

> @HarborSSH List my configured SSH hosts.

> @HarborSSH Run the identity task on dev and show the exit code and output.

> @HarborSSH On dev, run `git status --short` in `/home/chatgpt-runner/project`.

The last two requests await local approval by default. Approve the exact command
in the dashboard, then ask ChatGPT to retrieve the job's status and output.
HarborSSH does not itself send proactive ChatGPT messages when a job completes.

## Tools and behavior

| Tool | Behavior |
|---|---|
| `ssh_list_hosts` | Discover configured aliases and owner-defined tasks; no SSH connection. |
| `ssh_run` | Submit an exact command, optional directory and timeout; always local approval. |
| `ssh_run_task` | Submit a fixed locally configured task; optional explicit preapproval. |
| `ssh_get_job` | Read state, stdout, stderr, exit code and absolute byte cursors. |
| `ssh_list_jobs` | List this OAuth client's jobs. |
| `ssh_cancel_job` | Cancel a pending job or close the running SSH transport. |

Each execution is a **fresh SSH exec**, not a persistent terminal. `cd`, shell
variables and interactive state do not carry over. Supply `cwd` or put related
operations in one command. No PTY, interactive stdin, password prompts, agent
forwarding, SFTP tool, or automatic remote-process reattachment is implemented.
A timeout/cancellation closes the local SSH transport; detached remote processes
can survive, and partially completed changes are not rolled back.

Jobs return promptly and can run for the configured timeout, initially 300
seconds per host. Limits default to two running jobs globally, one per host, 64
retained in-memory jobs, and 512 KiB of output per job divided equally between
stdout/stderr. Larger streams discard oldest bytes and explicitly report that
loss. Output is memory-only; an atomic 24-hour idempotency ledger prevents
accepted request IDs being replayed automatically after a restart. See
[Architecture](docs/ARCHITECTURE.md) for exact failure and retention semantics.

## Make frequent tasks easier

Stop the bridge before editing `~/.harborssh/config.json`. Every new host has an
`identity` task with `autoApprove: false`. Change only a deliberately trusted
fixed task to automatic, for example:

```json
{
  "name": "identity",
  "description": "Show the dedicated runner's identity.",
  "command": "/usr/bin/id",
  "autoApprove": true
}
```

Tasks accept **no model-supplied arguments, command substitutions, cwd overrides,
or timeout overrides**. Set the host's `allowShell` to `false` for tasks-only use.
This is exact-command authorization, not proof a task is read-only: a build task
can execute project scripts, and binaries/scripts may change after configuration.
Free-form shell commands remain locally approved in every mode.

## Develop and test

The compiled release runs without `npm install`. To edit and rebuild sources:

```sh
npm install --ignore-scripts
npm run check
```

To test the supplied compiled release with no downloaded dependencies:

```sh
npm test
```

[Testing](docs/TESTING.md) explains the unit/in-process HTTP tests and the opt-in
live OpenSSH test. The delivered report is `TEST-RESULTS.txt`. The live SSH test
requires your explicitly authorized test host and is skipped otherwise. A
successful simulated-executor test is not represented as a real SSH session.

## Files and further reading

`src/` contains OAuth, transport, policy, job lifecycle, persistence, OpenSSH and
CLI modules. `dist/` is their compiled JavaScript. `web/` is the local dashboard.
`plugin/` contains the portable manifest and skill; `tests/` covers execution and
security boundaries. `deploy/` has deployment templates.

Export the portable plugin with your actual configured endpoint:

```sh
node dist/cli.js export-plugin --out ./harborssh-plugin
```

Exporting copies no keys, tokens, or host profiles. It does not publish/register
a plugin. See [Publication](docs/PUBLICATION.md) before distributing to others,
[Security](SECURITY.md) before connecting sensitive machines, and
[external references](docs/REFERENCES.md) for the protocols used.

MIT licensed. Not affiliated with OpenAI or OpenSSH.
