Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 4 additions & 6 deletions README.md
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
# ghost 👻

[![License: MIT](https://img.shields.io/badge/License-MIT-3DDC97?style=flat-square)](LICENSE)  ![Platform: macOS](https://img.shields.io/badge/platform-macOS-lightgrey?style=flat-square)  [![Built on Hermes Agent](https://img.shields.io/badge/built%20on-Hermes%20Agent-7C5CFF?style=flat-square)](https://github.com/NousResearch/hermes-agent)  ![Open-weight only](https://img.shields.io/badge/models-open--weight%20only-FF8A3D?style=flat-square)
[![License: MIT](https://img.shields.io/badge/License-MIT-3DDC97?style=flat-square)](LICENSE)  ![Platform](https://img.shields.io/badge/platform-macOS%20%C2%B7%20Linux%20%C2%B7%20WSL2-lightgrey?style=flat-square)  [![Built on Hermes Agent](https://img.shields.io/badge/built%20on-Hermes%20Agent-7C5CFF?style=flat-square)](https://github.com/NousResearch/hermes-agent)  ![Open-weight only](https://img.shields.io/badge/models-open--weight%20only-FF8A3D?style=flat-square)

**A private, unrestricted agentic harness.** A real terminal agent that runs commands, edits files, executes code, and searches the web, with every hosted request routed through OpenGradient's TEE gateway so the model provider never sees your prompts. It answers what you actually ask, drops to a fully-offline local model on demand, and phones home to no one.

Expand All@@ -12,7 +12,7 @@ Built on the [Hermes Agent](https://github.com/NousResearch/hermes-agent) engine

## Install (30 seconds)

macOS only. One deterministic command, no LLM and nothing agentic, installs **and** updates everything (the engine, the privacy stack, the `ghost` commands). uv provisions an isolated Python 3.11 under the hood, so the only prerequisite is `git`:
One deterministic command, no LLM and nothing agentic, installs **and** updates everything (the engine, the privacy stack, the `ghost` commands) on macOS, Linux, or WSL2. uv provisions an isolated Python 3.11 under the hood, so the only prerequisite is `git`:

```bash
curl -fsSL https://raw.githubusercontent.com/OpenGradient/ghost/main/install.sh | bash
Expand All@@ -29,9 +29,7 @@ Re-run the same command, or `ghost update`, to update. From a local clone it's j

## Why ghost exists

Most agents are either useless or creepy for real work. ghost fixes the two that matter most.

### #1: The Model Lectures You Instead of Working
### Problem #1: The Model Lectures You Instead of Working

> "The Net interprets censorship as damage and routes around it."
>
Expand All@@ -41,7 +39,7 @@ Most agents are either useless or creepy for real work. ghost fixes the two that

**The Fix.** ghost only connects **open-weight, unrestricted models** (DeepSeek V4 Pro by default; Hermes 4 405B/70B) and applies a per-model steer, so the default answers in full with no sermon. Closed, refusing models (Claude, GPT, Gemini, Grok) aren't offered, and the gateway rejects anything off the list. It treats you as a competent adult, but it isn't an edgelord either: it won't volunteer illegal or shock content, it just won't refuse you.

### #2: The Provider Reads Everything You Send
### Problem #2: The Provider Reads Everything You Send

> "Privacy is the power to selectively reveal oneself to the world."
>
Expand Down
64 changes: 39 additions & 25 deletions install.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -25,12 +25,13 @@
# GHOST_CHAT_APP_URL= override the website used for `ghost-login` (default chat.opengradient.ai)
set -euo pipefail

# macOS only: the privacy stack runs as launchd LaunchAgents and uses BSD tooling. Fail fast
# with a clear message rather than part-installing on Linux/WSL and erroring confusingly later.
if [ "$(uname -s)" != "Darwin" ]; then
echo "!! ghost's installer currently supports macOS only (it uses launchd). Detected: $(uname -s)." >&2
exit 1
fi
# Supported platforms: macOS (privacy services via launchd) and Linux / WSL2 (systemd --user,
# with a plain background-process fallback where systemd --user isn't available).
OS="$(uname -s)"
case "$OS" in
Darwin|Linux) ;;
*) echo "!! ghost supports macOS, Linux, and WSL2. Detected: $OS." >&2; exit 1 ;;
esac

# Resolve where this script lives. When run via `curl ... | bash` there is no checkout, so
# self-bootstrap: clone (or fast-forward) the repo into ~/.ghost-src and re-exec from there. This
Expand DownExpand Up@@ -90,7 +91,9 @@ have uv || { echo "!! ghost needs uv (https://docs.astral.sh/uv/getting-started/
if [ -n "$WANT_LOCAL" ]; then
if ! have ollama && have brew; then echo " installing Ollama (brew --cask)"; brew install --cask ollama || true; fi
have ollama || { echo "!! GHOST_LOCAL set but Ollama is missing -- install it from https://ollama.com (or drop GHOST_LOCAL for hosted-only) then re-run."; exit 1; }
pgrep -xq ollama || open -a Ollama 2>/dev/null || true ; sleep 1
if [ "$OS" = "Darwin" ]; then pgrep -xq ollama || open -a Ollama 2>/dev/null || true
else pgrep -xq ollama || (nohup ollama serve >/dev/null 2>&1 &) || true; fi
sleep 1
fi

if [ ! -d "$ENGINE_HOME/hermes-agent" ] && ! have hermes; then
Expand DownExpand Up@@ -193,25 +196,36 @@ else
rm -f "$PRIV/.scrub" "$PRIV/.no_scrub"
say "Full-fidelity mode (default) -- no outbound redaction. Set GHOST_SCRUB=1 to strip your PII/secrets before the gateway."
fi
mkdir -p "$LA"

# The scrubber runs as a launchd service; og-veil talks to chat-api directly (content is
# still private via OHTTP/TEE). Clean up any rotating-proxy marker from an older install.
BASE_SERVICES="hermes-pii-scrubber"
rm -f "$PRIV/.proxy"

for svc in $BASE_SERVICES; do
sed -e "s#__PYTHON__#$PYTHON#g" -e "s#__HOME__#$HOME#g" "$REPO/launchd/com.advait.$svc.plist" > "$LA/com.advait.$svc.plist"
launchctl unload "$LA/com.advait.$svc.plist" 2>/dev/null || true
launchctl load -w "$LA/com.advait.$svc.plist"
done
# Run the privacy services (scrubber :8788 + og-veil :11435) as managed, auto-restarting
# background services -- launchd on macOS, systemd --user on Linux/WSL2 (with a plain
# background-process fallback). Both talk to chat-api directly (content private via OHTTP/TEE).
rm -f "$PRIV/.proxy" # clean any rotating-proxy marker from an older install

# og-veil service (port 11435, to avoid colliding with Ollama on 11434). It owns the
# OHTTP/TEE/verification + auth, and talks to chat-api directly.
VEIL_PLIST="$LA/com.advait.hermes-veil.plist"
sed -e "s#__PYTHON__#$PYTHON#g" -e "s#__HOME__#$HOME#g" "$REPO/launchd/com.advait.hermes-veil.plist" > "$VEIL_PLIST"
launchctl unload "$VEIL_PLIST" 2>/dev/null || true
launchctl load -w "$VEIL_PLIST"
if [ "$OS" = "Darwin" ]; then
mkdir -p "$LA"
for svc in hermes-pii-scrubber hermes-veil; do
sed -e "s#__PYTHON__#$PYTHON#g" -e "s#__HOME__#$HOME#g" "$REPO/launchd/com.advait.$svc.plist" > "$LA/com.advait.$svc.plist"
launchctl unload "$LA/com.advait.$svc.plist" 2>/dev/null || true
launchctl load -w "$LA/com.advait.$svc.plist"
done
elif command -v systemctl >/dev/null 2>&1 && systemctl --user show-environment >/dev/null 2>&1; then
UD="$HOME/.config/systemd/user"; mkdir -p "$UD"
for svc in ghost-scrubber ghost-veil; do
sed -e "s#__PYTHON__#$PYTHON#g" -e "s#__PRIV__#$PRIV#g" "$REPO/systemd/$svc.service" > "$UD/$svc.service"
done
systemctl --user daemon-reload
systemctl --user reenable ghost-scrubber.service ghost-veil.service >/dev/null 2>&1 || true
systemctl --user restart ghost-scrubber.service ghost-veil.service
loginctl enable-linger "$USER" >/dev/null 2>&1 || true # keep services up without an active login (reboot persistence)
echo " services started via systemd --user (logs: journalctl --user -u ghost-veil)"
else
echo " systemd --user unavailable -- starting services as background processes"
echo " (no reboot persistence; re-run the installer or 'ghost update' to restart them)"
pkill -f "$PRIV/scrubbing_proxy.py" 2>/dev/null || true
pkill -f "veil serve --foreground --skip-setup --port 11435" 2>/dev/null || true
OG_VEIL_PORT=11435 nohup "$PYTHON" -m veil serve --foreground --skip-setup --port 11435 > "$PRIV/veil.out.log" 2>&1 &
nohup "$PYTHON" "$PRIV/scrubbing_proxy.py" > "$PRIV/scrubber.out.log" 2>&1 &
fi

printf " waiting for the scrubbing bridge"
for _ in $(seq 1 15); do [ "$(curl -s -o /dev/null -w '%{http_code}' --max-time 3 "$SCRUBBER/healthz" 2>/dev/null)" = 200 ] && break; printf "."; sleep 1; done; echo " up"
Expand Down
10 changes: 10 additions & 0 deletions systemd/ghost-scrubber.service
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
[Unit]
Description=ghost PII/secret scrubbing bridge (localhost :8788)

[Service]
ExecStart=__PYTHON__ __PRIV__/scrubbing_proxy.py
Restart=always
RestartSec=2

[Install]
WantedBy=default.target
11 changes: 11 additions & 0 deletions systemd/ghost-veil.service
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
[Unit]
Description=ghost og-veil transport (OHTTP relay + TEE verify, localhost :11435)

[Service]
Environment=OG_VEIL_PORT=11435
ExecStart=__PYTHON__ -m veil serve --foreground --skip-setup --port 11435
Restart=always
RestartSec=2

[Install]
WantedBy=default.target
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 4 additions & 6 deletions README.md
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
# ghost 👻

[![License: MIT](https://img.shields.io/badge/License-MIT-3DDC97?style=flat-square)](LICENSE)  ![Platform: macOS](https://img.shields.io/badge/platform-macOS-lightgrey?style=flat-square)  [![Built on Hermes Agent](https://img.shields.io/badge/built%20on-Hermes%20Agent-7C5CFF?style=flat-square)](https://github.com/NousResearch/hermes-agent)  ![Open-weight only](https://img.shields.io/badge/models-open--weight%20only-FF8A3D?style=flat-square)
[![License: MIT](https://img.shields.io/badge/License-MIT-3DDC97?style=flat-square)](LICENSE)  ![Platform](https://img.shields.io/badge/platform-macOS%20%C2%B7%20Linux%20%C2%B7%20WSL2-lightgrey?style=flat-square)  [![Built on Hermes Agent](https://img.shields.io/badge/built%20on-Hermes%20Agent-7C5CFF?style=flat-square)](https://github.com/NousResearch/hermes-agent)  ![Open-weight only](https://img.shields.io/badge/models-open--weight%20only-FF8A3D?style=flat-square)

**A private, unrestricted agentic harness.** A real terminal agent that runs commands, edits files, executes code, and searches the web, with every hosted request routed through OpenGradient's TEE gateway so the model provider never sees your prompts. It answers what you actually ask, drops to a fully-offline local model on demand, and phones home to no one.

Expand All@@ -12,7 +12,7 @@ Built on the [Hermes Agent](https://github.com/NousResearch/hermes-agent) engine

## Install (30 seconds)

macOS only. One deterministic command, no LLM and nothing agentic, installs **and** updates everything (the engine, the privacy stack, the `ghost` commands). uv provisions an isolated Python 3.11 under the hood, so the only prerequisite is `git`:
One deterministic command, no LLM and nothing agentic, installs **and** updates everything (the engine, the privacy stack, the `ghost` commands) on macOS, Linux, or WSL2. uv provisions an isolated Python 3.11 under the hood, so the only prerequisite is `git`:

```bash
curl -fsSL https://raw.githubusercontent.com/OpenGradient/ghost/main/install.sh | bash
Expand All@@ -29,9 +29,7 @@ Re-run the same command, or `ghost update`, to update. From a local clone it's j

## Why ghost exists

Most agents are either useless or creepy for real work. ghost fixes the two that matter most.

### #1: The Model Lectures You Instead of Working
### Problem #1: The Model Lectures You Instead of Working

> "The Net interprets censorship as damage and routes around it."
>
Expand All@@ -41,7 +39,7 @@ Most agents are either useless or creepy for real work. ghost fixes the two that

**The Fix.** ghost only connects **open-weight, unrestricted models** (DeepSeek V4 Pro by default; Hermes 4 405B/70B) and applies a per-model steer, so the default answers in full with no sermon. Closed, refusing models (Claude, GPT, Gemini, Grok) aren't offered, and the gateway rejects anything off the list. It treats you as a competent adult, but it isn't an edgelord either: it won't volunteer illegal or shock content, it just won't refuse you.

### #2: The Provider Reads Everything You Send
### Problem #2: The Provider Reads Everything You Send

> "Privacy is the power to selectively reveal oneself to the world."
>
Expand Down
64 changes: 39 additions & 25 deletions install.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -25,12 +25,13 @@
# GHOST_CHAT_APP_URL= override the website used for `ghost-login` (default chat.opengradient.ai)
set -euo pipefail

# macOS only: the privacy stack runs as launchd LaunchAgents and uses BSD tooling. Fail fast
# with a clear message rather than part-installing on Linux/WSL and erroring confusingly later.
if [ "$(uname -s)" != "Darwin" ]; then
echo "!! ghost's installer currently supports macOS only (it uses launchd). Detected: $(uname -s)." >&2
exit 1
fi
# Supported platforms: macOS (privacy services via launchd) and Linux / WSL2 (systemd --user,
# with a plain background-process fallback where systemd --user isn't available).
OS="$(uname -s)"
case "$OS" in
Darwin|Linux) ;;
*) echo "!! ghost supports macOS, Linux, and WSL2. Detected: $OS." >&2; exit 1 ;;
esac

# Resolve where this script lives. When run via `curl ... | bash` there is no checkout, so
# self-bootstrap: clone (or fast-forward) the repo into ~/.ghost-src and re-exec from there. This
Expand DownExpand Up@@ -90,7 +91,9 @@ have uv || { echo "!! ghost needs uv (https://docs.astral.sh/uv/getting-started/
if [ -n "$WANT_LOCAL" ]; then
if ! have ollama && have brew; then echo " installing Ollama (brew --cask)"; brew install --cask ollama || true; fi
have ollama || { echo "!! GHOST_LOCAL set but Ollama is missing -- install it from https://ollama.com (or drop GHOST_LOCAL for hosted-only) then re-run."; exit 1; }
pgrep -xq ollama || open -a Ollama 2>/dev/null || true ; sleep 1
if [ "$OS" = "Darwin" ]; then pgrep -xq ollama || open -a Ollama 2>/dev/null || true
else pgrep -xq ollama || (nohup ollama serve >/dev/null 2>&1 &) || true; fi
sleep 1
fi

if [ ! -d "$ENGINE_HOME/hermes-agent" ] && ! have hermes; then
Expand DownExpand Up@@ -193,25 +196,36 @@ else
rm -f "$PRIV/.scrub" "$PRIV/.no_scrub"
say "Full-fidelity mode (default) -- no outbound redaction. Set GHOST_SCRUB=1 to strip your PII/secrets before the gateway."
fi
mkdir -p "$LA"

# The scrubber runs as a launchd service; og-veil talks to chat-api directly (content is
# still private via OHTTP/TEE). Clean up any rotating-proxy marker from an older install.
BASE_SERVICES="hermes-pii-scrubber"
rm -f "$PRIV/.proxy"

for svc in $BASE_SERVICES; do
sed -e "s#__PYTHON__#$PYTHON#g" -e "s#__HOME__#$HOME#g" "$REPO/launchd/com.advait.$svc.plist" > "$LA/com.advait.$svc.plist"
launchctl unload "$LA/com.advait.$svc.plist" 2>/dev/null || true
launchctl load -w "$LA/com.advait.$svc.plist"
done
# Run the privacy services (scrubber :8788 + og-veil :11435) as managed, auto-restarting
# background services -- launchd on macOS, systemd --user on Linux/WSL2 (with a plain
# background-process fallback). Both talk to chat-api directly (content private via OHTTP/TEE).
rm -f "$PRIV/.proxy" # clean any rotating-proxy marker from an older install

# og-veil service (port 11435, to avoid colliding with Ollama on 11434). It owns the
# OHTTP/TEE/verification + auth, and talks to chat-api directly.
VEIL_PLIST="$LA/com.advait.hermes-veil.plist"
sed -e "s#__PYTHON__#$PYTHON#g" -e "s#__HOME__#$HOME#g" "$REPO/launchd/com.advait.hermes-veil.plist" > "$VEIL_PLIST"
launchctl unload "$VEIL_PLIST" 2>/dev/null || true
launchctl load -w "$VEIL_PLIST"
if [ "$OS" = "Darwin" ]; then
mkdir -p "$LA"
for svc in hermes-pii-scrubber hermes-veil; do
sed -e "s#__PYTHON__#$PYTHON#g" -e "s#__HOME__#$HOME#g" "$REPO/launchd/com.advait.$svc.plist" > "$LA/com.advait.$svc.plist"
launchctl unload "$LA/com.advait.$svc.plist" 2>/dev/null || true
launchctl load -w "$LA/com.advait.$svc.plist"
done
elif command -v systemctl >/dev/null 2>&1 && systemctl --user show-environment >/dev/null 2>&1; then
UD="$HOME/.config/systemd/user"; mkdir -p "$UD"
for svc in ghost-scrubber ghost-veil; do
sed -e "s#__PYTHON__#$PYTHON#g" -e "s#__PRIV__#$PRIV#g" "$REPO/systemd/$svc.service" > "$UD/$svc.service"
done
systemctl --user daemon-reload
systemctl --user reenable ghost-scrubber.service ghost-veil.service >/dev/null 2>&1 || true
systemctl --user restart ghost-scrubber.service ghost-veil.service
loginctl enable-linger "$USER" >/dev/null 2>&1 || true # keep services up without an active login (reboot persistence)
echo " services started via systemd --user (logs: journalctl --user -u ghost-veil)"
else
echo " systemd --user unavailable -- starting services as background processes"
echo " (no reboot persistence; re-run the installer or 'ghost update' to restart them)"
pkill -f "$PRIV/scrubbing_proxy.py" 2>/dev/null || true
pkill -f "veil serve --foreground --skip-setup --port 11435" 2>/dev/null || true
OG_VEIL_PORT=11435 nohup "$PYTHON" -m veil serve --foreground --skip-setup --port 11435 > "$PRIV/veil.out.log" 2>&1 &
nohup "$PYTHON" "$PRIV/scrubbing_proxy.py" > "$PRIV/scrubber.out.log" 2>&1 &
fi

printf " waiting for the scrubbing bridge"
for _ in $(seq 1 15); do [ "$(curl -s -o /dev/null -w '%{http_code}' --max-time 3 "$SCRUBBER/healthz" 2>/dev/null)" = 200 ] && break; printf "."; sleep 1; done; echo " up"
Expand Down
10 changes: 10 additions & 0 deletions systemd/ghost-scrubber.service
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
[Unit]
Description=ghost PII/secret scrubbing bridge (localhost :8788)

[Service]
ExecStart=__PYTHON__ __PRIV__/scrubbing_proxy.py
Restart=always
RestartSec=2

[Install]
WantedBy=default.target
11 changes: 11 additions & 0 deletions systemd/ghost-veil.service
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
[Unit]
Description=ghost og-veil transport (OHTTP relay + TEE verify, localhost :11435)

[Service]
Environment=OG_VEIL_PORT=11435
ExecStart=__PYTHON__ -m veil serve --foreground --skip-setup --port 11435
Restart=always
RestartSec=2

[Install]
WantedBy=default.target
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 4 additions & 6 deletions README.md
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
# ghost 👻

[![License: MIT](https://img.shields.io/badge/License-MIT-3DDC97?style=flat-square)](LICENSE)  ![Platform: macOS](https://img.shields.io/badge/platform-macOS-lightgrey?style=flat-square)  [![Built on Hermes Agent](https://img.shields.io/badge/built%20on-Hermes%20Agent-7C5CFF?style=flat-square)](https://github.com/NousResearch/hermes-agent)  ![Open-weight only](https://img.shields.io/badge/models-open--weight%20only-FF8A3D?style=flat-square)
[![License: MIT](https://img.shields.io/badge/License-MIT-3DDC97?style=flat-square)](LICENSE)  ![Platform](https://img.shields.io/badge/platform-macOS%20%C2%B7%20Linux%20%C2%B7%20WSL2-lightgrey?style=flat-square)  [![Built on Hermes Agent](https://img.shields.io/badge/built%20on-Hermes%20Agent-7C5CFF?style=flat-square)](https://github.com/NousResearch/hermes-agent)  ![Open-weight only](https://img.shields.io/badge/models-open--weight%20only-FF8A3D?style=flat-square)

**A private, unrestricted agentic harness.** A real terminal agent that runs commands, edits files, executes code, and searches the web, with every hosted request routed through OpenGradient's TEE gateway so the model provider never sees your prompts. It answers what you actually ask, drops to a fully-offline local model on demand, and phones home to no one.

Expand All@@ -12,7 +12,7 @@ Built on the [Hermes Agent](https://github.com/NousResearch/hermes-agent) engine

## Install (30 seconds)

macOS only. One deterministic command, no LLM and nothing agentic, installs **and** updates everything (the engine, the privacy stack, the `ghost` commands). uv provisions an isolated Python 3.11 under the hood, so the only prerequisite is `git`:
One deterministic command, no LLM and nothing agentic, installs **and** updates everything (the engine, the privacy stack, the `ghost` commands) on macOS, Linux, or WSL2. uv provisions an isolated Python 3.11 under the hood, so the only prerequisite is `git`:

```bash
curl -fsSL https://raw.githubusercontent.com/OpenGradient/ghost/main/install.sh | bash
Expand All@@ -29,9 +29,7 @@ Re-run the same command, or `ghost update`, to update. From a local clone it's j

## Why ghost exists

Most agents are either useless or creepy for real work. ghost fixes the two that matter most.

### #1: The Model Lectures You Instead of Working
### Problem #1: The Model Lectures You Instead of Working

> "The Net interprets censorship as damage and routes around it."
>
Expand All@@ -41,7 +39,7 @@ Most agents are either useless or creepy for real work. ghost fixes the two that

**The Fix.** ghost only connects **open-weight, unrestricted models** (DeepSeek V4 Pro by default; Hermes 4 405B/70B) and applies a per-model steer, so the default answers in full with no sermon. Closed, refusing models (Claude, GPT, Gemini, Grok) aren't offered, and the gateway rejects anything off the list. It treats you as a competent adult, but it isn't an edgelord either: it won't volunteer illegal or shock content, it just won't refuse you.

### #2: The Provider Reads Everything You Send
### Problem #2: The Provider Reads Everything You Send

> "Privacy is the power to selectively reveal oneself to the world."
>
Expand Down
64 changes: 39 additions & 25 deletions install.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -25,12 +25,13 @@
# GHOST_CHAT_APP_URL= override the website used for `ghost-login` (default chat.opengradient.ai)
set -euo pipefail

# macOS only: the privacy stack runs as launchd LaunchAgents and uses BSD tooling. Fail fast
# with a clear message rather than part-installing on Linux/WSL and erroring confusingly later.
if [ "$(uname -s)" != "Darwin" ]; then
echo "!! ghost's installer currently supports macOS only (it uses launchd). Detected: $(uname -s)." >&2
exit 1
fi
# Supported platforms: macOS (privacy services via launchd) and Linux / WSL2 (systemd --user,
# with a plain background-process fallback where systemd --user isn't available).
OS="$(uname -s)"
case "$OS" in
Darwin|Linux) ;;
*) echo "!! ghost supports macOS, Linux, and WSL2. Detected: $OS." >&2; exit 1 ;;
esac

# Resolve where this script lives. When run via `curl ... | bash` there is no checkout, so
# self-bootstrap: clone (or fast-forward) the repo into ~/.ghost-src and re-exec from there. This
Expand DownExpand Up@@ -90,7 +91,9 @@ have uv || { echo "!! ghost needs uv (https://docs.astral.sh/uv/getting-started/
if [ -n "$WANT_LOCAL" ]; then
if ! have ollama && have brew; then echo " installing Ollama (brew --cask)"; brew install --cask ollama || true; fi
have ollama || { echo "!! GHOST_LOCAL set but Ollama is missing -- install it from https://ollama.com (or drop GHOST_LOCAL for hosted-only) then re-run."; exit 1; }
pgrep -xq ollama || open -a Ollama 2>/dev/null || true ; sleep 1
if [ "$OS" = "Darwin" ]; then pgrep -xq ollama || open -a Ollama 2>/dev/null || true
else pgrep -xq ollama || (nohup ollama serve >/dev/null 2>&1 &) || true; fi
sleep 1
fi

if [ ! -d "$ENGINE_HOME/hermes-agent" ] && ! have hermes; then
Expand DownExpand Up@@ -193,25 +196,36 @@ else
rm -f "$PRIV/.scrub" "$PRIV/.no_scrub"
say "Full-fidelity mode (default) -- no outbound redaction. Set GHOST_SCRUB=1 to strip your PII/secrets before the gateway."
fi
mkdir -p "$LA"

# The scrubber runs as a launchd service; og-veil talks to chat-api directly (content is
# still private via OHTTP/TEE). Clean up any rotating-proxy marker from an older install.
BASE_SERVICES="hermes-pii-scrubber"
rm -f "$PRIV/.proxy"

for svc in $BASE_SERVICES; do
sed -e "s#__PYTHON__#$PYTHON#g" -e "s#__HOME__#$HOME#g" "$REPO/launchd/com.advait.$svc.plist" > "$LA/com.advait.$svc.plist"
launchctl unload "$LA/com.advait.$svc.plist" 2>/dev/null || true
launchctl load -w "$LA/com.advait.$svc.plist"
done
# Run the privacy services (scrubber :8788 + og-veil :11435) as managed, auto-restarting
# background services -- launchd on macOS, systemd --user on Linux/WSL2 (with a plain
# background-process fallback). Both talk to chat-api directly (content private via OHTTP/TEE).
rm -f "$PRIV/.proxy" # clean any rotating-proxy marker from an older install

# og-veil service (port 11435, to avoid colliding with Ollama on 11434). It owns the
# OHTTP/TEE/verification + auth, and talks to chat-api directly.
VEIL_PLIST="$LA/com.advait.hermes-veil.plist"
sed -e "s#__PYTHON__#$PYTHON#g" -e "s#__HOME__#$HOME#g" "$REPO/launchd/com.advait.hermes-veil.plist" > "$VEIL_PLIST"
launchctl unload "$VEIL_PLIST" 2>/dev/null || true
launchctl load -w "$VEIL_PLIST"
if [ "$OS" = "Darwin" ]; then
mkdir -p "$LA"
for svc in hermes-pii-scrubber hermes-veil; do
sed -e "s#__PYTHON__#$PYTHON#g" -e "s#__HOME__#$HOME#g" "$REPO/launchd/com.advait.$svc.plist" > "$LA/com.advait.$svc.plist"
launchctl unload "$LA/com.advait.$svc.plist" 2>/dev/null || true
launchctl load -w "$LA/com.advait.$svc.plist"
done
elif command -v systemctl >/dev/null 2>&1 && systemctl --user show-environment >/dev/null 2>&1; then
UD="$HOME/.config/systemd/user"; mkdir -p "$UD"
for svc in ghost-scrubber ghost-veil; do
sed -e "s#__PYTHON__#$PYTHON#g" -e "s#__PRIV__#$PRIV#g" "$REPO/systemd/$svc.service" > "$UD/$svc.service"
done
systemctl --user daemon-reload
systemctl --user reenable ghost-scrubber.service ghost-veil.service >/dev/null 2>&1 || true
systemctl --user restart ghost-scrubber.service ghost-veil.service
loginctl enable-linger "$USER" >/dev/null 2>&1 || true # keep services up without an active login (reboot persistence)
echo " services started via systemd --user (logs: journalctl --user -u ghost-veil)"
else
echo " systemd --user unavailable -- starting services as background processes"
echo " (no reboot persistence; re-run the installer or 'ghost update' to restart them)"
pkill -f "$PRIV/scrubbing_proxy.py" 2>/dev/null || true
pkill -f "veil serve --foreground --skip-setup --port 11435" 2>/dev/null || true
OG_VEIL_PORT=11435 nohup "$PYTHON" -m veil serve --foreground --skip-setup --port 11435 > "$PRIV/veil.out.log" 2>&1 &
nohup "$PYTHON" "$PRIV/scrubbing_proxy.py" > "$PRIV/scrubber.out.log" 2>&1 &
fi

printf " waiting for the scrubbing bridge"
for _ in $(seq 1 15); do [ "$(curl -s -o /dev/null -w '%{http_code}' --max-time 3 "$SCRUBBER/healthz" 2>/dev/null)" = 200 ] && break; printf "."; sleep 1; done; echo " up"
Expand Down
10 changes: 10 additions & 0 deletions systemd/ghost-scrubber.service
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
[Unit]
Description=ghost PII/secret scrubbing bridge (localhost :8788)

[Service]
ExecStart=__PYTHON__ __PRIV__/scrubbing_proxy.py
Restart=always
RestartSec=2

[Install]
WantedBy=default.target
11 changes: 11 additions & 0 deletions systemd/ghost-veil.service
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
[Unit]
Description=ghost og-veil transport (OHTTP relay + TEE verify, localhost :11435)

[Service]
Environment=OG_VEIL_PORT=11435
ExecStart=__PYTHON__ -m veil serve --foreground --skip-setup --port 11435
Restart=always
RestartSec=2

[Install]
WantedBy=default.target
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 4 additions & 6 deletions README.md
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
# ghost 👻

[![License: MIT](https://img.shields.io/badge/License-MIT-3DDC97?style=flat-square)](LICENSE)  ![Platform: macOS](https://img.shields.io/badge/platform-macOS-lightgrey?style=flat-square)  [![Built on Hermes Agent](https://img.shields.io/badge/built%20on-Hermes%20Agent-7C5CFF?style=flat-square)](https://github.com/NousResearch/hermes-agent)  ![Open-weight only](https://img.shields.io/badge/models-open--weight%20only-FF8A3D?style=flat-square)
[![License: MIT](https://img.shields.io/badge/License-MIT-3DDC97?style=flat-square)](LICENSE)  ![Platform](https://img.shields.io/badge/platform-macOS%20%C2%B7%20Linux%20%C2%B7%20WSL2-lightgrey?style=flat-square)  [![Built on Hermes Agent](https://img.shields.io/badge/built%20on-Hermes%20Agent-7C5CFF?style=flat-square)](https://github.com/NousResearch/hermes-agent)  ![Open-weight only](https://img.shields.io/badge/models-open--weight%20only-FF8A3D?style=flat-square)

**A private, unrestricted agentic harness.** A real terminal agent that runs commands, edits files, executes code, and searches the web, with every hosted request routed through OpenGradient's TEE gateway so the model provider never sees your prompts. It answers what you actually ask, drops to a fully-offline local model on demand, and phones home to no one.

Expand All@@ -12,7 +12,7 @@ Built on the [Hermes Agent](https://github.com/NousResearch/hermes-agent) engine

## Install (30 seconds)

macOS only. One deterministic command, no LLM and nothing agentic, installs **and** updates everything (the engine, the privacy stack, the `ghost` commands). uv provisions an isolated Python 3.11 under the hood, so the only prerequisite is `git`:
One deterministic command, no LLM and nothing agentic, installs **and** updates everything (the engine, the privacy stack, the `ghost` commands) on macOS, Linux, or WSL2. uv provisions an isolated Python 3.11 under the hood, so the only prerequisite is `git`:

```bash
curl -fsSL https://raw.githubusercontent.com/OpenGradient/ghost/main/install.sh | bash
Expand All@@ -29,9 +29,7 @@ Re-run the same command, or `ghost update`, to update. From a local clone it's j

## Why ghost exists

Most agents are either useless or creepy for real work. ghost fixes the two that matter most.

### #1: The Model Lectures You Instead of Working
### Problem #1: The Model Lectures You Instead of Working

> "The Net interprets censorship as damage and routes around it."
>
Expand All@@ -41,7 +39,7 @@ Most agents are either useless or creepy for real work. ghost fixes the two that

**The Fix.** ghost only connects **open-weight, unrestricted models** (DeepSeek V4 Pro by default; Hermes 4 405B/70B) and applies a per-model steer, so the default answers in full with no sermon. Closed, refusing models (Claude, GPT, Gemini, Grok) aren't offered, and the gateway rejects anything off the list. It treats you as a competent adult, but it isn't an edgelord either: it won't volunteer illegal or shock content, it just won't refuse you.

### #2: The Provider Reads Everything You Send
### Problem #2: The Provider Reads Everything You Send

> "Privacy is the power to selectively reveal oneself to the world."
>
Expand Down
64 changes: 39 additions & 25 deletions install.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -25,12 +25,13 @@
# GHOST_CHAT_APP_URL= override the website used for `ghost-login` (default chat.opengradient.ai)
set -euo pipefail

# macOS only: the privacy stack runs as launchd LaunchAgents and uses BSD tooling. Fail fast
# with a clear message rather than part-installing on Linux/WSL and erroring confusingly later.
if [ "$(uname -s)" != "Darwin" ]; then
echo "!! ghost's installer currently supports macOS only (it uses launchd). Detected: $(uname -s)." >&2
exit 1
fi
# Supported platforms: macOS (privacy services via launchd) and Linux / WSL2 (systemd --user,
# with a plain background-process fallback where systemd --user isn't available).
OS="$(uname -s)"
case "$OS" in
Darwin|Linux) ;;
*) echo "!! ghost supports macOS, Linux, and WSL2. Detected: $OS." >&2; exit 1 ;;
esac

# Resolve where this script lives. When run via `curl ... | bash` there is no checkout, so
# self-bootstrap: clone (or fast-forward) the repo into ~/.ghost-src and re-exec from there. This
Expand DownExpand Up@@ -90,7 +91,9 @@ have uv || { echo "!! ghost needs uv (https://docs.astral.sh/uv/getting-started/
if [ -n "$WANT_LOCAL" ]; then
if ! have ollama && have brew; then echo " installing Ollama (brew --cask)"; brew install --cask ollama || true; fi
have ollama || { echo "!! GHOST_LOCAL set but Ollama is missing -- install it from https://ollama.com (or drop GHOST_LOCAL for hosted-only) then re-run."; exit 1; }
pgrep -xq ollama || open -a Ollama 2>/dev/null || true ; sleep 1
if [ "$OS" = "Darwin" ]; then pgrep -xq ollama || open -a Ollama 2>/dev/null || true
else pgrep -xq ollama || (nohup ollama serve >/dev/null 2>&1 &) || true; fi
sleep 1
fi

if [ ! -d "$ENGINE_HOME/hermes-agent" ] && ! have hermes; then
Expand DownExpand Up@@ -193,25 +196,36 @@ else
rm -f "$PRIV/.scrub" "$PRIV/.no_scrub"
say "Full-fidelity mode (default) -- no outbound redaction. Set GHOST_SCRUB=1 to strip your PII/secrets before the gateway."
fi
mkdir -p "$LA"

# The scrubber runs as a launchd service; og-veil talks to chat-api directly (content is
# still private via OHTTP/TEE). Clean up any rotating-proxy marker from an older install.
BASE_SERVICES="hermes-pii-scrubber"
rm -f "$PRIV/.proxy"

for svc in $BASE_SERVICES; do
sed -e "s#__PYTHON__#$PYTHON#g" -e "s#__HOME__#$HOME#g" "$REPO/launchd/com.advait.$svc.plist" > "$LA/com.advait.$svc.plist"
launchctl unload "$LA/com.advait.$svc.plist" 2>/dev/null || true
launchctl load -w "$LA/com.advait.$svc.plist"
done
# Run the privacy services (scrubber :8788 + og-veil :11435) as managed, auto-restarting
# background services -- launchd on macOS, systemd --user on Linux/WSL2 (with a plain
# background-process fallback). Both talk to chat-api directly (content private via OHTTP/TEE).
rm -f "$PRIV/.proxy" # clean any rotating-proxy marker from an older install

# og-veil service (port 11435, to avoid colliding with Ollama on 11434). It owns the
# OHTTP/TEE/verification + auth, and talks to chat-api directly.
VEIL_PLIST="$LA/com.advait.hermes-veil.plist"
sed -e "s#__PYTHON__#$PYTHON#g" -e "s#__HOME__#$HOME#g" "$REPO/launchd/com.advait.hermes-veil.plist" > "$VEIL_PLIST"
launchctl unload "$VEIL_PLIST" 2>/dev/null || true
launchctl load -w "$VEIL_PLIST"
if [ "$OS" = "Darwin" ]; then
mkdir -p "$LA"
for svc in hermes-pii-scrubber hermes-veil; do
sed -e "s#__PYTHON__#$PYTHON#g" -e "s#__HOME__#$HOME#g" "$REPO/launchd/com.advait.$svc.plist" > "$LA/com.advait.$svc.plist"
launchctl unload "$LA/com.advait.$svc.plist" 2>/dev/null || true
launchctl load -w "$LA/com.advait.$svc.plist"
done
elif command -v systemctl >/dev/null 2>&1 && systemctl --user show-environment >/dev/null 2>&1; then
UD="$HOME/.config/systemd/user"; mkdir -p "$UD"
for svc in ghost-scrubber ghost-veil; do
sed -e "s#__PYTHON__#$PYTHON#g" -e "s#__PRIV__#$PRIV#g" "$REPO/systemd/$svc.service" > "$UD/$svc.service"
done
systemctl --user daemon-reload
systemctl --user reenable ghost-scrubber.service ghost-veil.service >/dev/null 2>&1 || true
systemctl --user restart ghost-scrubber.service ghost-veil.service
loginctl enable-linger "$USER" >/dev/null 2>&1 || true # keep services up without an active login (reboot persistence)
echo " services started via systemd --user (logs: journalctl --user -u ghost-veil)"
else
echo " systemd --user unavailable -- starting services as background processes"
echo " (no reboot persistence; re-run the installer or 'ghost update' to restart them)"
pkill -f "$PRIV/scrubbing_proxy.py" 2>/dev/null || true
pkill -f "veil serve --foreground --skip-setup --port 11435" 2>/dev/null || true
OG_VEIL_PORT=11435 nohup "$PYTHON" -m veil serve --foreground --skip-setup --port 11435 > "$PRIV/veil.out.log" 2>&1 &
nohup "$PYTHON" "$PRIV/scrubbing_proxy.py" > "$PRIV/scrubber.out.log" 2>&1 &
fi

printf " waiting for the scrubbing bridge"
for _ in $(seq 1 15); do [ "$(curl -s -o /dev/null -w '%{http_code}' --max-time 3 "$SCRUBBER/healthz" 2>/dev/null)" = 200 ] && break; printf "."; sleep 1; done; echo " up"
Expand Down
10 changes: 10 additions & 0 deletions systemd/ghost-scrubber.service
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
[Unit]
Description=ghost PII/secret scrubbing bridge (localhost :8788)

[Service]
ExecStart=__PYTHON__ __PRIV__/scrubbing_proxy.py
Restart=always
RestartSec=2

[Install]
WantedBy=default.target
11 changes: 11 additions & 0 deletions systemd/ghost-veil.service
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
[Unit]
Description=ghost og-veil transport (OHTTP relay + TEE verify, localhost :11435)

[Service]
Environment=OG_VEIL_PORT=11435
ExecStart=__PYTHON__ -m veil serve --foreground --skip-setup --port 11435
Restart=always
RestartSec=2

[Install]
WantedBy=default.target
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 4 additions & 6 deletions README.md
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
# ghost 👻

[![License: MIT](https://img.shields.io/badge/License-MIT-3DDC97?style=flat-square)](LICENSE)  ![Platform: macOS](https://img.shields.io/badge/platform-macOS-lightgrey?style=flat-square)  [![Built on Hermes Agent](https://img.shields.io/badge/built%20on-Hermes%20Agent-7C5CFF?style=flat-square)](https://github.com/NousResearch/hermes-agent)  ![Open-weight only](https://img.shields.io/badge/models-open--weight%20only-FF8A3D?style=flat-square)
[![License: MIT](https://img.shields.io/badge/License-MIT-3DDC97?style=flat-square)](LICENSE)  ![Platform](https://img.shields.io/badge/platform-macOS%20%C2%B7%20Linux%20%C2%B7%20WSL2-lightgrey?style=flat-square)  [![Built on Hermes Agent](https://img.shields.io/badge/built%20on-Hermes%20Agent-7C5CFF?style=flat-square)](https://github.com/NousResearch/hermes-agent)  ![Open-weight only](https://img.shields.io/badge/models-open--weight%20only-FF8A3D?style=flat-square)

**A private, unrestricted agentic harness.** A real terminal agent that runs commands, edits files, executes code, and searches the web, with every hosted request routed through OpenGradient's TEE gateway so the model provider never sees your prompts. It answers what you actually ask, drops to a fully-offline local model on demand, and phones home to no one.

Expand All@@ -12,7 +12,7 @@ Built on the [Hermes Agent](https://github.com/NousResearch/hermes-agent) engine

## Install (30 seconds)

macOS only. One deterministic command, no LLM and nothing agentic, installs **and** updates everything (the engine, the privacy stack, the `ghost` commands). uv provisions an isolated Python 3.11 under the hood, so the only prerequisite is `git`:
One deterministic command, no LLM and nothing agentic, installs **and** updates everything (the engine, the privacy stack, the `ghost` commands) on macOS, Linux, or WSL2. uv provisions an isolated Python 3.11 under the hood, so the only prerequisite is `git`:

```bash
curl -fsSL https://raw.githubusercontent.com/OpenGradient/ghost/main/install.sh | bash
Expand All@@ -29,9 +29,7 @@ Re-run the same command, or `ghost update`, to update. From a local clone it's j

## Why ghost exists

Most agents are either useless or creepy for real work. ghost fixes the two that matter most.

### #1: The Model Lectures You Instead of Working
### Problem #1: The Model Lectures You Instead of Working

> "The Net interprets censorship as damage and routes around it."
>
Expand All@@ -41,7 +39,7 @@ Most agents are either useless or creepy for real work. ghost fixes the two that

**The Fix.** ghost only connects **open-weight, unrestricted models** (DeepSeek V4 Pro by default; Hermes 4 405B/70B) and applies a per-model steer, so the default answers in full with no sermon. Closed, refusing models (Claude, GPT, Gemini, Grok) aren't offered, and the gateway rejects anything off the list. It treats you as a competent adult, but it isn't an edgelord either: it won't volunteer illegal or shock content, it just won't refuse you.

### #2: The Provider Reads Everything You Send
### Problem #2: The Provider Reads Everything You Send

> "Privacy is the power to selectively reveal oneself to the world."
>
Expand Down
64 changes: 39 additions & 25 deletions install.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -25,12 +25,13 @@
# GHOST_CHAT_APP_URL= override the website used for `ghost-login` (default chat.opengradient.ai)
set -euo pipefail

# macOS only: the privacy stack runs as launchd LaunchAgents and uses BSD tooling. Fail fast
# with a clear message rather than part-installing on Linux/WSL and erroring confusingly later.
if [ "$(uname -s)" != "Darwin" ]; then
echo "!! ghost's installer currently supports macOS only (it uses launchd). Detected: $(uname -s)." >&2
exit 1
fi
# Supported platforms: macOS (privacy services via launchd) and Linux / WSL2 (systemd --user,
# with a plain background-process fallback where systemd --user isn't available).
OS="$(uname -s)"
case "$OS" in
Darwin|Linux) ;;
*) echo "!! ghost supports macOS, Linux, and WSL2. Detected: $OS." >&2; exit 1 ;;
esac

# Resolve where this script lives. When run via `curl ... | bash` there is no checkout, so
# self-bootstrap: clone (or fast-forward) the repo into ~/.ghost-src and re-exec from there. This
Expand DownExpand Up@@ -90,7 +91,9 @@ have uv || { echo "!! ghost needs uv (https://docs.astral.sh/uv/getting-started/
if [ -n "$WANT_LOCAL" ]; then
if ! have ollama && have brew; then echo " installing Ollama (brew --cask)"; brew install --cask ollama || true; fi
have ollama || { echo "!! GHOST_LOCAL set but Ollama is missing -- install it from https://ollama.com (or drop GHOST_LOCAL for hosted-only) then re-run."; exit 1; }
pgrep -xq ollama || open -a Ollama 2>/dev/null || true ; sleep 1
if [ "$OS" = "Darwin" ]; then pgrep -xq ollama || open -a Ollama 2>/dev/null || true
else pgrep -xq ollama || (nohup ollama serve >/dev/null 2>&1 &) || true; fi
sleep 1
fi

if [ ! -d "$ENGINE_HOME/hermes-agent" ] && ! have hermes; then
Expand DownExpand Up@@ -193,25 +196,36 @@ else
rm -f "$PRIV/.scrub" "$PRIV/.no_scrub"
say "Full-fidelity mode (default) -- no outbound redaction. Set GHOST_SCRUB=1 to strip your PII/secrets before the gateway."
fi
mkdir -p "$LA"

# The scrubber runs as a launchd service; og-veil talks to chat-api directly (content is
# still private via OHTTP/TEE). Clean up any rotating-proxy marker from an older install.
BASE_SERVICES="hermes-pii-scrubber"
rm -f "$PRIV/.proxy"

for svc in $BASE_SERVICES; do
sed -e "s#__PYTHON__#$PYTHON#g" -e "s#__HOME__#$HOME#g" "$REPO/launchd/com.advait.$svc.plist" > "$LA/com.advait.$svc.plist"
launchctl unload "$LA/com.advait.$svc.plist" 2>/dev/null || true
launchctl load -w "$LA/com.advait.$svc.plist"
done
# Run the privacy services (scrubber :8788 + og-veil :11435) as managed, auto-restarting
# background services -- launchd on macOS, systemd --user on Linux/WSL2 (with a plain
# background-process fallback). Both talk to chat-api directly (content private via OHTTP/TEE).
rm -f "$PRIV/.proxy" # clean any rotating-proxy marker from an older install

# og-veil service (port 11435, to avoid colliding with Ollama on 11434). It owns the
# OHTTP/TEE/verification + auth, and talks to chat-api directly.
VEIL_PLIST="$LA/com.advait.hermes-veil.plist"
sed -e "s#__PYTHON__#$PYTHON#g" -e "s#__HOME__#$HOME#g" "$REPO/launchd/com.advait.hermes-veil.plist" > "$VEIL_PLIST"
launchctl unload "$VEIL_PLIST" 2>/dev/null || true
launchctl load -w "$VEIL_PLIST"
if [ "$OS" = "Darwin" ]; then
mkdir -p "$LA"
for svc in hermes-pii-scrubber hermes-veil; do
sed -e "s#__PYTHON__#$PYTHON#g" -e "s#__HOME__#$HOME#g" "$REPO/launchd/com.advait.$svc.plist" > "$LA/com.advait.$svc.plist"
launchctl unload "$LA/com.advait.$svc.plist" 2>/dev/null || true
launchctl load -w "$LA/com.advait.$svc.plist"
done
elif command -v systemctl >/dev/null 2>&1 && systemctl --user show-environment >/dev/null 2>&1; then
UD="$HOME/.config/systemd/user"; mkdir -p "$UD"
for svc in ghost-scrubber ghost-veil; do
sed -e "s#__PYTHON__#$PYTHON#g" -e "s#__PRIV__#$PRIV#g" "$REPO/systemd/$svc.service" > "$UD/$svc.service"
done
systemctl --user daemon-reload
systemctl --user reenable ghost-scrubber.service ghost-veil.service >/dev/null 2>&1 || true
systemctl --user restart ghost-scrubber.service ghost-veil.service
loginctl enable-linger "$USER" >/dev/null 2>&1 || true # keep services up without an active login (reboot persistence)
echo " services started via systemd --user (logs: journalctl --user -u ghost-veil)"
else
echo " systemd --user unavailable -- starting services as background processes"
echo " (no reboot persistence; re-run the installer or 'ghost update' to restart them)"
pkill -f "$PRIV/scrubbing_proxy.py" 2>/dev/null || true
pkill -f "veil serve --foreground --skip-setup --port 11435" 2>/dev/null || true
OG_VEIL_PORT=11435 nohup "$PYTHON" -m veil serve --foreground --skip-setup --port 11435 > "$PRIV/veil.out.log" 2>&1 &
nohup "$PYTHON" "$PRIV/scrubbing_proxy.py" > "$PRIV/scrubber.out.log" 2>&1 &
fi

printf " waiting for the scrubbing bridge"
for _ in $(seq 1 15); do [ "$(curl -s -o /dev/null -w '%{http_code}' --max-time 3 "$SCRUBBER/healthz" 2>/dev/null)" = 200 ] && break; printf "."; sleep 1; done; echo " up"
Expand Down
10 changes: 10 additions & 0 deletions systemd/ghost-scrubber.service
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
[Unit]
Description=ghost PII/secret scrubbing bridge (localhost :8788)

[Service]
ExecStart=__PYTHON__ __PRIV__/scrubbing_proxy.py
Restart=always
RestartSec=2

[Install]
WantedBy=default.target
11 changes: 11 additions & 0 deletions systemd/ghost-veil.service
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
[Unit]
Description=ghost og-veil transport (OHTTP relay + TEE verify, localhost :11435)

[Service]
Environment=OG_VEIL_PORT=11435
ExecStart=__PYTHON__ -m veil serve --foreground --skip-setup --port 11435
Restart=always
RestartSec=2

[Install]
WantedBy=default.target
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 4 additions & 6 deletions README.md
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
# ghost 👻

[![License: MIT](https://img.shields.io/badge/License-MIT-3DDC97?style=flat-square)](LICENSE)  ![Platform: macOS](https://img.shields.io/badge/platform-macOS-lightgrey?style=flat-square)  [![Built on Hermes Agent](https://img.shields.io/badge/built%20on-Hermes%20Agent-7C5CFF?style=flat-square)](https://github.com/NousResearch/hermes-agent)  ![Open-weight only](https://img.shields.io/badge/models-open--weight%20only-FF8A3D?style=flat-square)
[![License: MIT](https://img.shields.io/badge/License-MIT-3DDC97?style=flat-square)](LICENSE)  ![Platform](https://img.shields.io/badge/platform-macOS%20%C2%B7%20Linux%20%C2%B7%20WSL2-lightgrey?style=flat-square)  [![Built on Hermes Agent](https://img.shields.io/badge/built%20on-Hermes%20Agent-7C5CFF?style=flat-square)](https://github.com/NousResearch/hermes-agent)  ![Open-weight only](https://img.shields.io/badge/models-open--weight%20only-FF8A3D?style=flat-square)

**A private, unrestricted agentic harness.** A real terminal agent that runs commands, edits files, executes code, and searches the web, with every hosted request routed through OpenGradient's TEE gateway so the model provider never sees your prompts. It answers what you actually ask, drops to a fully-offline local model on demand, and phones home to no one.

Expand All@@ -12,7 +12,7 @@ Built on the [Hermes Agent](https://github.com/NousResearch/hermes-agent) engine

## Install (30 seconds)

macOS only. One deterministic command, no LLM and nothing agentic, installs **and** updates everything (the engine, the privacy stack, the `ghost` commands). uv provisions an isolated Python 3.11 under the hood, so the only prerequisite is `git`:
One deterministic command, no LLM and nothing agentic, installs **and** updates everything (the engine, the privacy stack, the `ghost` commands) on macOS, Linux, or WSL2. uv provisions an isolated Python 3.11 under the hood, so the only prerequisite is `git`:

```bash
curl -fsSL https://raw.githubusercontent.com/OpenGradient/ghost/main/install.sh | bash
Expand All@@ -29,9 +29,7 @@ Re-run the same command, or `ghost update`, to update. From a local clone it's j

## Why ghost exists

Most agents are either useless or creepy for real work. ghost fixes the two that matter most.

### #1: The Model Lectures You Instead of Working
### Problem #1: The Model Lectures You Instead of Working

> "The Net interprets censorship as damage and routes around it."
>
Expand All@@ -41,7 +39,7 @@ Most agents are either useless or creepy for real work. ghost fixes the two that

**The Fix.** ghost only connects **open-weight, unrestricted models** (DeepSeek V4 Pro by default; Hermes 4 405B/70B) and applies a per-model steer, so the default answers in full with no sermon. Closed, refusing models (Claude, GPT, Gemini, Grok) aren't offered, and the gateway rejects anything off the list. It treats you as a competent adult, but it isn't an edgelord either: it won't volunteer illegal or shock content, it just won't refuse you.

### #2: The Provider Reads Everything You Send
### Problem #2: The Provider Reads Everything You Send

> "Privacy is the power to selectively reveal oneself to the world."
>
Expand Down
64 changes: 39 additions & 25 deletions install.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -25,12 +25,13 @@
# GHOST_CHAT_APP_URL= override the website used for `ghost-login` (default chat.opengradient.ai)
set -euo pipefail

# macOS only: the privacy stack runs as launchd LaunchAgents and uses BSD tooling. Fail fast
# with a clear message rather than part-installing on Linux/WSL and erroring confusingly later.
if [ "$(uname -s)" != "Darwin" ]; then
echo "!! ghost's installer currently supports macOS only (it uses launchd). Detected: $(uname -s)." >&2
exit 1
fi
# Supported platforms: macOS (privacy services via launchd) and Linux / WSL2 (systemd --user,
# with a plain background-process fallback where systemd --user isn't available).
OS="$(uname -s)"
case "$OS" in
Darwin|Linux) ;;
*) echo "!! ghost supports macOS, Linux, and WSL2. Detected: $OS." >&2; exit 1 ;;
esac

# Resolve where this script lives. When run via `curl ... | bash` there is no checkout, so
# self-bootstrap: clone (or fast-forward) the repo into ~/.ghost-src and re-exec from there. This
Expand DownExpand Up@@ -90,7 +91,9 @@ have uv || { echo "!! ghost needs uv (https://docs.astral.sh/uv/getting-started/
if [ -n "$WANT_LOCAL" ]; then
if ! have ollama && have brew; then echo " installing Ollama (brew --cask)"; brew install --cask ollama || true; fi
have ollama || { echo "!! GHOST_LOCAL set but Ollama is missing -- install it from https://ollama.com (or drop GHOST_LOCAL for hosted-only) then re-run."; exit 1; }
pgrep -xq ollama || open -a Ollama 2>/dev/null || true ; sleep 1
if [ "$OS" = "Darwin" ]; then pgrep -xq ollama || open -a Ollama 2>/dev/null || true
else pgrep -xq ollama || (nohup ollama serve >/dev/null 2>&1 &) || true; fi
sleep 1
fi

if [ ! -d "$ENGINE_HOME/hermes-agent" ] && ! have hermes; then
Expand DownExpand Up@@ -193,25 +196,36 @@ else
rm -f "$PRIV/.scrub" "$PRIV/.no_scrub"
say "Full-fidelity mode (default) -- no outbound redaction. Set GHOST_SCRUB=1 to strip your PII/secrets before the gateway."
fi
mkdir -p "$LA"

# The scrubber runs as a launchd service; og-veil talks to chat-api directly (content is
# still private via OHTTP/TEE). Clean up any rotating-proxy marker from an older install.
BASE_SERVICES="hermes-pii-scrubber"
rm -f "$PRIV/.proxy"

for svc in $BASE_SERVICES; do
sed -e "s#__PYTHON__#$PYTHON#g" -e "s#__HOME__#$HOME#g" "$REPO/launchd/com.advait.$svc.plist" > "$LA/com.advait.$svc.plist"
launchctl unload "$LA/com.advait.$svc.plist" 2>/dev/null || true
launchctl load -w "$LA/com.advait.$svc.plist"
done
# Run the privacy services (scrubber :8788 + og-veil :11435) as managed, auto-restarting
# background services -- launchd on macOS, systemd --user on Linux/WSL2 (with a plain
# background-process fallback). Both talk to chat-api directly (content private via OHTTP/TEE).
rm -f "$PRIV/.proxy" # clean any rotating-proxy marker from an older install

# og-veil service (port 11435, to avoid colliding with Ollama on 11434). It owns the
# OHTTP/TEE/verification + auth, and talks to chat-api directly.
VEIL_PLIST="$LA/com.advait.hermes-veil.plist"
sed -e "s#__PYTHON__#$PYTHON#g" -e "s#__HOME__#$HOME#g" "$REPO/launchd/com.advait.hermes-veil.plist" > "$VEIL_PLIST"
launchctl unload "$VEIL_PLIST" 2>/dev/null || true
launchctl load -w "$VEIL_PLIST"
if [ "$OS" = "Darwin" ]; then
mkdir -p "$LA"
for svc in hermes-pii-scrubber hermes-veil; do
sed -e "s#__PYTHON__#$PYTHON#g" -e "s#__HOME__#$HOME#g" "$REPO/launchd/com.advait.$svc.plist" > "$LA/com.advait.$svc.plist"
launchctl unload "$LA/com.advait.$svc.plist" 2>/dev/null || true
launchctl load -w "$LA/com.advait.$svc.plist"
done
elif command -v systemctl >/dev/null 2>&1 && systemctl --user show-environment >/dev/null 2>&1; then
UD="$HOME/.config/systemd/user"; mkdir -p "$UD"
for svc in ghost-scrubber ghost-veil; do
sed -e "s#__PYTHON__#$PYTHON#g" -e "s#__PRIV__#$PRIV#g" "$REPO/systemd/$svc.service" > "$UD/$svc.service"
done
systemctl --user daemon-reload
systemctl --user reenable ghost-scrubber.service ghost-veil.service >/dev/null 2>&1 || true
systemctl --user restart ghost-scrubber.service ghost-veil.service
loginctl enable-linger "$USER" >/dev/null 2>&1 || true # keep services up without an active login (reboot persistence)
echo " services started via systemd --user (logs: journalctl --user -u ghost-veil)"
else
echo " systemd --user unavailable -- starting services as background processes"
echo " (no reboot persistence; re-run the installer or 'ghost update' to restart them)"
pkill -f "$PRIV/scrubbing_proxy.py" 2>/dev/null || true
pkill -f "veil serve --foreground --skip-setup --port 11435" 2>/dev/null || true
OG_VEIL_PORT=11435 nohup "$PYTHON" -m veil serve --foreground --skip-setup --port 11435 > "$PRIV/veil.out.log" 2>&1 &
nohup "$PYTHON" "$PRIV/scrubbing_proxy.py" > "$PRIV/scrubber.out.log" 2>&1 &
fi

printf " waiting for the scrubbing bridge"
for _ in $(seq 1 15); do [ "$(curl -s -o /dev/null -w '%{http_code}' --max-time 3 "$SCRUBBER/healthz" 2>/dev/null)" = 200 ] && break; printf "."; sleep 1; done; echo " up"
Expand Down
10 changes: 10 additions & 0 deletions systemd/ghost-scrubber.service
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
[Unit]
Description=ghost PII/secret scrubbing bridge (localhost :8788)

[Service]
ExecStart=__PYTHON__ __PRIV__/scrubbing_proxy.py
Restart=always
RestartSec=2

[Install]
WantedBy=default.target
11 changes: 11 additions & 0 deletions systemd/ghost-veil.service
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
[Unit]
Description=ghost og-veil transport (OHTTP relay + TEE verify, localhost :11435)

[Service]
Environment=OG_VEIL_PORT=11435
ExecStart=__PYTHON__ -m veil serve --foreground --skip-setup --port 11435
Restart=always
RestartSec=2

[Install]
WantedBy=default.target
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 4 additions & 6 deletions README.md
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
# ghost 👻

[![License: MIT](https://img.shields.io/badge/License-MIT-3DDC97?style=flat-square)](LICENSE)  ![Platform: macOS](https://img.shields.io/badge/platform-macOS-lightgrey?style=flat-square)  [![Built on Hermes Agent](https://img.shields.io/badge/built%20on-Hermes%20Agent-7C5CFF?style=flat-square)](https://github.com/NousResearch/hermes-agent)  ![Open-weight only](https://img.shields.io/badge/models-open--weight%20only-FF8A3D?style=flat-square)
[![License: MIT](https://img.shields.io/badge/License-MIT-3DDC97?style=flat-square)](LICENSE)  ![Platform](https://img.shields.io/badge/platform-macOS%20%C2%B7%20Linux%20%C2%B7%20WSL2-lightgrey?style=flat-square)  [![Built on Hermes Agent](https://img.shields.io/badge/built%20on-Hermes%20Agent-7C5CFF?style=flat-square)](https://github.com/NousResearch/hermes-agent)  ![Open-weight only](https://img.shields.io/badge/models-open--weight%20only-FF8A3D?style=flat-square)

**A private, unrestricted agentic harness.** A real terminal agent that runs commands, edits files, executes code, and searches the web, with every hosted request routed through OpenGradient's TEE gateway so the model provider never sees your prompts. It answers what you actually ask, drops to a fully-offline local model on demand, and phones home to no one.

Expand All@@ -12,7 +12,7 @@ Built on the [Hermes Agent](https://github.com/NousResearch/hermes-agent) engine

## Install (30 seconds)

macOS only. One deterministic command, no LLM and nothing agentic, installs **and** updates everything (the engine, the privacy stack, the `ghost` commands). uv provisions an isolated Python 3.11 under the hood, so the only prerequisite is `git`:
One deterministic command, no LLM and nothing agentic, installs **and** updates everything (the engine, the privacy stack, the `ghost` commands) on macOS, Linux, or WSL2. uv provisions an isolated Python 3.11 under the hood, so the only prerequisite is `git`:

```bash
curl -fsSL https://raw.githubusercontent.com/OpenGradient/ghost/main/install.sh | bash
Expand All@@ -29,9 +29,7 @@ Re-run the same command, or `ghost update`, to update. From a local clone it's j

## Why ghost exists

Most agents are either useless or creepy for real work. ghost fixes the two that matter most.

### #1: The Model Lectures You Instead of Working
### Problem #1: The Model Lectures You Instead of Working

> "The Net interprets censorship as damage and routes around it."
>
Expand All@@ -41,7 +39,7 @@ Most agents are either useless or creepy for real work. ghost fixes the two that

**The Fix.** ghost only connects **open-weight, unrestricted models** (DeepSeek V4 Pro by default; Hermes 4 405B/70B) and applies a per-model steer, so the default answers in full with no sermon. Closed, refusing models (Claude, GPT, Gemini, Grok) aren't offered, and the gateway rejects anything off the list. It treats you as a competent adult, but it isn't an edgelord either: it won't volunteer illegal or shock content, it just won't refuse you.

### #2: The Provider Reads Everything You Send
### Problem #2: The Provider Reads Everything You Send

> "Privacy is the power to selectively reveal oneself to the world."
>
Expand Down
64 changes: 39 additions & 25 deletions install.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -25,12 +25,13 @@
# GHOST_CHAT_APP_URL= override the website used for `ghost-login` (default chat.opengradient.ai)
set -euo pipefail

# macOS only: the privacy stack runs as launchd LaunchAgents and uses BSD tooling. Fail fast
# with a clear message rather than part-installing on Linux/WSL and erroring confusingly later.
if [ "$(uname -s)" != "Darwin" ]; then
echo "!! ghost's installer currently supports macOS only (it uses launchd). Detected: $(uname -s)." >&2
exit 1
fi
# Supported platforms: macOS (privacy services via launchd) and Linux / WSL2 (systemd --user,
# with a plain background-process fallback where systemd --user isn't available).
OS="$(uname -s)"
case "$OS" in
Darwin|Linux) ;;
*) echo "!! ghost supports macOS, Linux, and WSL2. Detected: $OS." >&2; exit 1 ;;
esac

# Resolve where this script lives. When run via `curl ... | bash` there is no checkout, so
# self-bootstrap: clone (or fast-forward) the repo into ~/.ghost-src and re-exec from there. This
Expand DownExpand Up@@ -90,7 +91,9 @@ have uv || { echo "!! ghost needs uv (https://docs.astral.sh/uv/getting-started/
if [ -n "$WANT_LOCAL" ]; then
if ! have ollama && have brew; then echo " installing Ollama (brew --cask)"; brew install --cask ollama || true; fi
have ollama || { echo "!! GHOST_LOCAL set but Ollama is missing -- install it from https://ollama.com (or drop GHOST_LOCAL for hosted-only) then re-run."; exit 1; }
pgrep -xq ollama || open -a Ollama 2>/dev/null || true ; sleep 1
if [ "$OS" = "Darwin" ]; then pgrep -xq ollama || open -a Ollama 2>/dev/null || true
else pgrep -xq ollama || (nohup ollama serve >/dev/null 2>&1 &) || true; fi
sleep 1
fi

if [ ! -d "$ENGINE_HOME/hermes-agent" ] && ! have hermes; then
Expand DownExpand Up@@ -193,25 +196,36 @@ else
rm -f "$PRIV/.scrub" "$PRIV/.no_scrub"
say "Full-fidelity mode (default) -- no outbound redaction. Set GHOST_SCRUB=1 to strip your PII/secrets before the gateway."
fi
mkdir -p "$LA"

# The scrubber runs as a launchd service; og-veil talks to chat-api directly (content is
# still private via OHTTP/TEE). Clean up any rotating-proxy marker from an older install.
BASE_SERVICES="hermes-pii-scrubber"
rm -f "$PRIV/.proxy"

for svc in $BASE_SERVICES; do
sed -e "s#__PYTHON__#$PYTHON#g" -e "s#__HOME__#$HOME#g" "$REPO/launchd/com.advait.$svc.plist" > "$LA/com.advait.$svc.plist"
launchctl unload "$LA/com.advait.$svc.plist" 2>/dev/null || true
launchctl load -w "$LA/com.advait.$svc.plist"
done
# Run the privacy services (scrubber :8788 + og-veil :11435) as managed, auto-restarting
# background services -- launchd on macOS, systemd --user on Linux/WSL2 (with a plain
# background-process fallback). Both talk to chat-api directly (content private via OHTTP/TEE).
rm -f "$PRIV/.proxy" # clean any rotating-proxy marker from an older install

# og-veil service (port 11435, to avoid colliding with Ollama on 11434). It owns the
# OHTTP/TEE/verification + auth, and talks to chat-api directly.
VEIL_PLIST="$LA/com.advait.hermes-veil.plist"
sed -e "s#__PYTHON__#$PYTHON#g" -e "s#__HOME__#$HOME#g" "$REPO/launchd/com.advait.hermes-veil.plist" > "$VEIL_PLIST"
launchctl unload "$VEIL_PLIST" 2>/dev/null || true
launchctl load -w "$VEIL_PLIST"
if [ "$OS" = "Darwin" ]; then
mkdir -p "$LA"
for svc in hermes-pii-scrubber hermes-veil; do
sed -e "s#__PYTHON__#$PYTHON#g" -e "s#__HOME__#$HOME#g" "$REPO/launchd/com.advait.$svc.plist" > "$LA/com.advait.$svc.plist"
launchctl unload "$LA/com.advait.$svc.plist" 2>/dev/null || true
launchctl load -w "$LA/com.advait.$svc.plist"
done
elif command -v systemctl >/dev/null 2>&1 && systemctl --user show-environment >/dev/null 2>&1; then
UD="$HOME/.config/systemd/user"; mkdir -p "$UD"
for svc in ghost-scrubber ghost-veil; do
sed -e "s#__PYTHON__#$PYTHON#g" -e "s#__PRIV__#$PRIV#g" "$REPO/systemd/$svc.service" > "$UD/$svc.service"
done
systemctl --user daemon-reload
systemctl --user reenable ghost-scrubber.service ghost-veil.service >/dev/null 2>&1 || true
systemctl --user restart ghost-scrubber.service ghost-veil.service
loginctl enable-linger "$USER" >/dev/null 2>&1 || true # keep services up without an active login (reboot persistence)
echo " services started via systemd --user (logs: journalctl --user -u ghost-veil)"
else
echo " systemd --user unavailable -- starting services as background processes"
echo " (no reboot persistence; re-run the installer or 'ghost update' to restart them)"
pkill -f "$PRIV/scrubbing_proxy.py" 2>/dev/null || true
pkill -f "veil serve --foreground --skip-setup --port 11435" 2>/dev/null || true
OG_VEIL_PORT=11435 nohup "$PYTHON" -m veil serve --foreground --skip-setup --port 11435 > "$PRIV/veil.out.log" 2>&1 &
nohup "$PYTHON" "$PRIV/scrubbing_proxy.py" > "$PRIV/scrubber.out.log" 2>&1 &
fi

printf " waiting for the scrubbing bridge"
for _ in $(seq 1 15); do [ "$(curl -s -o /dev/null -w '%{http_code}' --max-time 3 "$SCRUBBER/healthz" 2>/dev/null)" = 200 ] && break; printf "."; sleep 1; done; echo " up"
Expand Down
10 changes: 10 additions & 0 deletions systemd/ghost-scrubber.service
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
[Unit]
Description=ghost PII/secret scrubbing bridge (localhost :8788)

[Service]
ExecStart=__PYTHON__ __PRIV__/scrubbing_proxy.py
Restart=always
RestartSec=2

[Install]
WantedBy=default.target
11 changes: 11 additions & 0 deletions systemd/ghost-veil.service
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
[Unit]
Description=ghost og-veil transport (OHTTP relay + TEE verify, localhost :11435)

[Service]
Environment=OG_VEIL_PORT=11435
ExecStart=__PYTHON__ -m veil serve --foreground --skip-setup --port 11435
Restart=always
RestartSec=2

[Install]
WantedBy=default.target
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 4 additions & 6 deletions README.md
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
# ghost 👻

[![License: MIT](https://img.shields.io/badge/License-MIT-3DDC97?style=flat-square)](LICENSE)  ![Platform: macOS](https://img.shields.io/badge/platform-macOS-lightgrey?style=flat-square)  [![Built on Hermes Agent](https://img.shields.io/badge/built%20on-Hermes%20Agent-7C5CFF?style=flat-square)](https://github.com/NousResearch/hermes-agent)  ![Open-weight only](https://img.shields.io/badge/models-open--weight%20only-FF8A3D?style=flat-square)
[![License: MIT](https://img.shields.io/badge/License-MIT-3DDC97?style=flat-square)](LICENSE)  ![Platform](https://img.shields.io/badge/platform-macOS%20%C2%B7%20Linux%20%C2%B7%20WSL2-lightgrey?style=flat-square)  [![Built on Hermes Agent](https://img.shields.io/badge/built%20on-Hermes%20Agent-7C5CFF?style=flat-square)](https://github.com/NousResearch/hermes-agent)  ![Open-weight only](https://img.shields.io/badge/models-open--weight%20only-FF8A3D?style=flat-square)

**A private, unrestricted agentic harness.** A real terminal agent that runs commands, edits files, executes code, and searches the web, with every hosted request routed through OpenGradient's TEE gateway so the model provider never sees your prompts. It answers what you actually ask, drops to a fully-offline local model on demand, and phones home to no one.

Expand All@@ -12,7 +12,7 @@ Built on the [Hermes Agent](https://github.com/NousResearch/hermes-agent) engine

## Install (30 seconds)

macOS only. One deterministic command, no LLM and nothing agentic, installs **and** updates everything (the engine, the privacy stack, the `ghost` commands). uv provisions an isolated Python 3.11 under the hood, so the only prerequisite is `git`:
One deterministic command, no LLM and nothing agentic, installs **and** updates everything (the engine, the privacy stack, the `ghost` commands) on macOS, Linux, or WSL2. uv provisions an isolated Python 3.11 under the hood, so the only prerequisite is `git`:

```bash
curl -fsSL https://raw.githubusercontent.com/OpenGradient/ghost/main/install.sh | bash
Expand All@@ -29,9 +29,7 @@ Re-run the same command, or `ghost update`, to update. From a local clone it's j

## Why ghost exists

Most agents are either useless or creepy for real work. ghost fixes the two that matter most.

### #1: The Model Lectures You Instead of Working
### Problem #1: The Model Lectures You Instead of Working

> "The Net interprets censorship as damage and routes around it."
>
Expand All@@ -41,7 +39,7 @@ Most agents are either useless or creepy for real work. ghost fixes the two that

**The Fix.** ghost only connects **open-weight, unrestricted models** (DeepSeek V4 Pro by default; Hermes 4 405B/70B) and applies a per-model steer, so the default answers in full with no sermon. Closed, refusing models (Claude, GPT, Gemini, Grok) aren't offered, and the gateway rejects anything off the list. It treats you as a competent adult, but it isn't an edgelord either: it won't volunteer illegal or shock content, it just won't refuse you.

### #2: The Provider Reads Everything You Send
### Problem #2: The Provider Reads Everything You Send

> "Privacy is the power to selectively reveal oneself to the world."
>
Expand Down
64 changes: 39 additions & 25 deletions install.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -25,12 +25,13 @@
# GHOST_CHAT_APP_URL= override the website used for `ghost-login` (default chat.opengradient.ai)
set -euo pipefail

# macOS only: the privacy stack runs as launchd LaunchAgents and uses BSD tooling. Fail fast
# with a clear message rather than part-installing on Linux/WSL and erroring confusingly later.
if [ "$(uname -s)" != "Darwin" ]; then
echo "!! ghost's installer currently supports macOS only (it uses launchd). Detected: $(uname -s)." >&2
exit 1
fi
# Supported platforms: macOS (privacy services via launchd) and Linux / WSL2 (systemd --user,
# with a plain background-process fallback where systemd --user isn't available).
OS="$(uname -s)"
case "$OS" in
Darwin|Linux) ;;
*) echo "!! ghost supports macOS, Linux, and WSL2. Detected: $OS." >&2; exit 1 ;;
esac

# Resolve where this script lives. When run via `curl ... | bash` there is no checkout, so
# self-bootstrap: clone (or fast-forward) the repo into ~/.ghost-src and re-exec from there. This
Expand DownExpand Up@@ -90,7 +91,9 @@ have uv || { echo "!! ghost needs uv (https://docs.astral.sh/uv/getting-started/
if [ -n "$WANT_LOCAL" ]; then
if ! have ollama && have brew; then echo " installing Ollama (brew --cask)"; brew install --cask ollama || true; fi
have ollama || { echo "!! GHOST_LOCAL set but Ollama is missing -- install it from https://ollama.com (or drop GHOST_LOCAL for hosted-only) then re-run."; exit 1; }
pgrep -xq ollama || open -a Ollama 2>/dev/null || true ; sleep 1
if [ "$OS" = "Darwin" ]; then pgrep -xq ollama || open -a Ollama 2>/dev/null || true
else pgrep -xq ollama || (nohup ollama serve >/dev/null 2>&1 &) || true; fi
sleep 1
fi

if [ ! -d "$ENGINE_HOME/hermes-agent" ] && ! have hermes; then
Expand DownExpand Up@@ -193,25 +196,36 @@ else
rm -f "$PRIV/.scrub" "$PRIV/.no_scrub"
say "Full-fidelity mode (default) -- no outbound redaction. Set GHOST_SCRUB=1 to strip your PII/secrets before the gateway."
fi
mkdir -p "$LA"

# The scrubber runs as a launchd service; og-veil talks to chat-api directly (content is
# still private via OHTTP/TEE). Clean up any rotating-proxy marker from an older install.
BASE_SERVICES="hermes-pii-scrubber"
rm -f "$PRIV/.proxy"

for svc in $BASE_SERVICES; do
sed -e "s#__PYTHON__#$PYTHON#g" -e "s#__HOME__#$HOME#g" "$REPO/launchd/com.advait.$svc.plist" > "$LA/com.advait.$svc.plist"
launchctl unload "$LA/com.advait.$svc.plist" 2>/dev/null || true
launchctl load -w "$LA/com.advait.$svc.plist"
done
# Run the privacy services (scrubber :8788 + og-veil :11435) as managed, auto-restarting
# background services -- launchd on macOS, systemd --user on Linux/WSL2 (with a plain
# background-process fallback). Both talk to chat-api directly (content private via OHTTP/TEE).
rm -f "$PRIV/.proxy" # clean any rotating-proxy marker from an older install

# og-veil service (port 11435, to avoid colliding with Ollama on 11434). It owns the
# OHTTP/TEE/verification + auth, and talks to chat-api directly.
VEIL_PLIST="$LA/com.advait.hermes-veil.plist"
sed -e "s#__PYTHON__#$PYTHON#g" -e "s#__HOME__#$HOME#g" "$REPO/launchd/com.advait.hermes-veil.plist" > "$VEIL_PLIST"
launchctl unload "$VEIL_PLIST" 2>/dev/null || true
launchctl load -w "$VEIL_PLIST"
if [ "$OS" = "Darwin" ]; then
mkdir -p "$LA"
for svc in hermes-pii-scrubber hermes-veil; do
sed -e "s#__PYTHON__#$PYTHON#g" -e "s#__HOME__#$HOME#g" "$REPO/launchd/com.advait.$svc.plist" > "$LA/com.advait.$svc.plist"
launchctl unload "$LA/com.advait.$svc.plist" 2>/dev/null || true
launchctl load -w "$LA/com.advait.$svc.plist"
done
elif command -v systemctl >/dev/null 2>&1 && systemctl --user show-environment >/dev/null 2>&1; then
UD="$HOME/.config/systemd/user"; mkdir -p "$UD"
for svc in ghost-scrubber ghost-veil; do
sed -e "s#__PYTHON__#$PYTHON#g" -e "s#__PRIV__#$PRIV#g" "$REPO/systemd/$svc.service" > "$UD/$svc.service"
done
systemctl --user daemon-reload
systemctl --user reenable ghost-scrubber.service ghost-veil.service >/dev/null 2>&1 || true
systemctl --user restart ghost-scrubber.service ghost-veil.service
loginctl enable-linger "$USER" >/dev/null 2>&1 || true # keep services up without an active login (reboot persistence)
echo " services started via systemd --user (logs: journalctl --user -u ghost-veil)"
else
echo " systemd --user unavailable -- starting services as background processes"
echo " (no reboot persistence; re-run the installer or 'ghost update' to restart them)"
pkill -f "$PRIV/scrubbing_proxy.py" 2>/dev/null || true
pkill -f "veil serve --foreground --skip-setup --port 11435" 2>/dev/null || true
OG_VEIL_PORT=11435 nohup "$PYTHON" -m veil serve --foreground --skip-setup --port 11435 > "$PRIV/veil.out.log" 2>&1 &
nohup "$PYTHON" "$PRIV/scrubbing_proxy.py" > "$PRIV/scrubber.out.log" 2>&1 &
fi

printf " waiting for the scrubbing bridge"
for _ in $(seq 1 15); do [ "$(curl -s -o /dev/null -w '%{http_code}' --max-time 3 "$SCRUBBER/healthz" 2>/dev/null)" = 200 ] && break; printf "."; sleep 1; done; echo " up"
Expand Down
10 changes: 10 additions & 0 deletions systemd/ghost-scrubber.service
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
[Unit]
Description=ghost PII/secret scrubbing bridge (localhost :8788)

[Service]
ExecStart=__PYTHON__ __PRIV__/scrubbing_proxy.py
Restart=always
RestartSec=2

[Install]
WantedBy=default.target
11 changes: 11 additions & 0 deletions systemd/ghost-veil.service
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
[Unit]
Description=ghost og-veil transport (OHTTP relay + TEE verify, localhost :11435)

[Service]
Environment=OG_VEIL_PORT=11435
ExecStart=__PYTHON__ -m veil serve --foreground --skip-setup --port 11435
Restart=always
RestartSec=2

[Install]
WantedBy=default.target
Loading