Deploy a Remote Server
Install a Fabric server on Linux, connect securely, and preserve its data during updates.
Run Fabric Agents on a server to keep conversations and tool execution on that machine. Connect with the Fabric desktop app or the bundled browser UI. Use the same release for the server and desktop app.
The released server archive currently supports Linux x64 with glibc. It includes Bun and the Fabric runtime assets. The host also needs Python 3.12 or newer, Node.js (24 is tested), GitHub CLI with access to the release repository, sha256sum, and systemd user services. Install prerequisites through your host's normal administration process. A macOS desktop can connect to a Linux server; a macOS server archive is not available yet.
Use a dedicated Unix service account and a new, private server profile. A private profile separates configuration and inherited credentials; it is not an operating-system sandbox. Tools retain the Unix account's file permissions and can access its other files by absolute path. Each server owns its provider sign-ins. The server token grants access to the whole server, including its workspaces; use separate service accounts and server profiles for users who should not share that access.
Download and verify
These commands run on the server, in Bash. The release repository currently requires authenticated access. Sign in to GitHub on this host with gh auth login if needed; do not transfer an existing credentials directory from another machine. Replace the release version when a newer matching server and desktop release is available. Stop if any command fails, and continue only after the checksum reports OK.
umask 077
export FABRIC_SERVER_RELEASE=0.14.19
export FABRIC_SERVER_ROOT="$HOME/.local/share/fabric-agent-server"
mkdir -p -m 700 "$FABRIC_SERVER_ROOT/downloads"
cd "$FABRIC_SERVER_ROOT/downloads"
archive="fabric-agent-server-${FABRIC_SERVER_RELEASE}-linux-x64.tar.gz"
gh release download "v${FABRIC_SERVER_RELEASE}" --repo Fabric-Pro/fabric-agents \
--pattern "$archive" --pattern "$archive.sha256" --dir .
sha256sum --check "$archive.sha256"Download both files from the same release. The checksum detects a damaged or mismatched download; it is not a signature from a separate trust source. For a host without repository access, an authorized user can transfer just the archive and checksum downloaded from that release. Never transfer credentials with them.
Extract into a new version directory and verify its release metadata:
python3 -I - <<'PY'
import hashlib, json, os, platform, sys, tarfile
from pathlib import Path
assert sys.version_info >= (3, 12)
assert platform.system() == 'Linux' and platform.machine() == 'x86_64'
os.umask(0o077)
root = Path(os.environ['FABRIC_SERVER_ROOT'])
version = os.environ['FABRIC_SERVER_RELEASE']
assert root.is_absolute() and not any(p.is_symlink() for p in (root, *root.parents))
parent = root / 'app'
assert not parent.is_symlink()
parent.mkdir(mode=0o700, exist_ok=True)
app = parent / version
assert not app.exists() and not app.is_symlink()
archive = root / 'downloads' / f'fabric-agent-server-{version}-linux-x64.tar.gz'
expected, filename = Path(str(archive) + '.sha256').read_text().split()
assert filename == archive.name and len(expected) == 64
with archive.open('rb') as payload:
assert hashlib.file_digest(payload, 'sha256').hexdigest() == expected
payload.seek(0)
app.mkdir(mode=0o700) # Refuse to overwrite an installed version.
with tarfile.open(fileobj=payload, mode='r:gz') as bundle:
bundle.extractall(app, filter='data')
metadata = json.loads((app / 'fabric-server-build.json').read_text())
assert metadata['version'] == version and metadata['platform'] == 'linux-x64'
assert metadata['candidate'] is False and len(metadata['commit']) == 40
print('Verified server release:', version)
PYCreate a private profile and service
Run the following once for a fresh profile. It refuses to replace an existing environment file, data directory, home directory or service. It generates a token locally without displaying it. Keep the file private; do not use shell tracing or paste it into support messages.
The isolated home and configuration prevent importing another Fabric profile. Use paths without spaces for this setup. The small launcher reads only the listed environment values, so provider credentials from a login shell are not inherited by the server or its tools.
python3 -I - <<'PY'
import json, os, secrets, stat, subprocess
from pathlib import Path
os.umask(0o077)
root = Path(os.environ['FABRIC_SERVER_ROOT'])
version = os.environ['FABRIC_SERVER_RELEASE']
app = root / 'app' / version
unit_dir = Path.home() / '.config/systemd/user'
for path in (root, app, unit_dir):
assert path.is_absolute() and not any(p.is_symlink() for p in (path, *path.parents))
assert app.is_dir()
assert not (root / 'server.env').exists()
assert not (root / 'home').exists() and not (root / 'data').exists()
assert not (root / 'app/current').exists() and not (root / 'app/current').is_symlink()
unit = unit_dir / 'fabric-agent-server.service'
assert not unit.exists() and not unit.is_symlink()
node = Path(subprocess.check_output(['node', '-p', 'process.execPath'], text=True).strip())
assert node.is_absolute() and os.access(node, os.X_OK)
for relative in ('home', 'home/.config', 'home/.cache', 'home/.local/share', 'data/workspaces', 'tmp'):
(root / relative).mkdir(mode=0o700, parents=True, exist_ok=True)
home = root / 'home'
env = {
'HOME': str(home), 'USERPROFILE': str(home),
'XDG_CONFIG_HOME': str(home / '.config'), 'XDG_CACHE_HOME': str(home / '.cache'),
'XDG_DATA_HOME': str(home / '.local/share'), 'TMPDIR': str(root / 'tmp'),
'FABRIC_SERVER_TOKEN': secrets.token_hex(32), 'FABRIC_SERVER_QUIET_TOKEN': '1',
'FABRIC_RPC_HOST': '127.0.0.1', 'FABRIC_RPC_PORT': '9100',
'FABRIC_CONFIG_DIR': str(root / 'data'), 'FABRIC_SKIP_CREDENTIALS_MIGRATION': '1',
'FABRIC_APP_ROOT': str(app), 'FABRIC_RESOURCES_PATH': str(app / 'resources'),
'FABRIC_BUNDLED_ASSETS_ROOT': str(app), 'FABRIC_UV': str(app / 'resources/bin/uv'),
'FABRIC_SCRIPTS': str(app / 'resources/scripts'), 'FABRIC_BUN': str(app / 'vendor/bun/bun'),
'FABRIC_CLI_ENTRY': str(app / 'apps/cli/src/index.ts'),
'FABRIC_WEBUI_DIR': str(app / 'apps/webui/dist'),
'FABRIC_MESSAGING_WA_WORKER': str(app / 'packages/messaging-whatsapp-worker/dist/worker.cjs'),
'FABRIC_MESSAGING_NODE_BIN': str(node), 'FABRIC_VERSION': version, 'FABRIC_IS_PACKAGED': 'true',
'PATH': f'{app}/resources/bin:{app}/vendor/bun:{node.parent}:/usr/local/bin:/usr/bin:/bin',
}
# Keep systemd EnvironmentFile values and paths unambiguous.
assert all(v.isascii() and not any(c.isspace() or c in '\\"%' for c in v) for v in env.values())
for key in ('FABRIC_UV', 'FABRIC_BUN', 'FABRIC_MESSAGING_NODE_BIN'):
assert os.access(env[key], os.X_OK), key
for key in ('FABRIC_CLI_ENTRY', 'FABRIC_MESSAGING_WA_WORKER', 'FABRIC_WEBUI_DIR'):
assert Path(env[key]).exists(), key
with (root / 'server.env').open('x') as output:
output.write(''.join(f'{key}={json.dumps(value)}\n' for key, value in env.items()))
assert stat.S_IMODE((root / 'server.env').stat().st_mode) == 0o600
launcher = root / 'run.fabric.py'
with launcher.open('x') as output:
output.write('import json, os, stat, sys\nfrom pathlib import Path\n')
output.write('file = Path(__file__).with_name("server.env")\n')
output.write('assert not file.is_symlink() and stat.S_IMODE(file.stat().st_mode) == 0o600\n')
output.write('env = dict((k, json.loads(v)) for k, v in (line.split("=", 1) for line in file.read_text().splitlines()))\n')
output.write(f'assert set(env) == set({tuple(env)!r})\n')
output.write('assert env["FABRIC_RPC_HOST"] == "127.0.0.1"\n')
output.write('app = Path(env["FABRIC_APP_ROOT"])\n')
output.write('entry = env["FABRIC_CLI_ENTRY"] if len(sys.argv) > 1 else str(app / "packages/server/src/index.ts")\n')
output.write('if len(sys.argv) > 1: env["FABRIC_SERVER_URL"] = "ws://127.0.0.1:" + env["FABRIC_RPC_PORT"]\n')
output.write('os.chdir(app)\nos.execve(env["FABRIC_BUN"], [env["FABRIC_BUN"], "run", entry, *sys.argv[1:]], env)\n')
(root / 'app/current').symlink_to(version, target_is_directory=True)
unit_dir.mkdir(mode=0o700, parents=True, exist_ok=True)
with unit.open('x') as output:
output.write(f'''[Unit]
Description=Fabric Agents server
After=network.target
[Service]
WorkingDirectory={app}
EnvironmentFile={root}/server.env
ExecStart=/usr/bin/python3 -I "{launcher}"
Restart=on-failure
UMask=0077
[Install]
WantedBy=default.target
''')
print('Created private Fabric profile and user service; token was not displayed.')
PYLeave port 9100 free for this profile, or change FABRIC_RPC_PORT privately before starting. Start and check the service:
systemctl --user daemon-reload
systemctl --user start fabric-agent-server.service
systemctl --user is-active fabric-agent-server.service
python3 -I "$FABRIC_SERVER_ROOT/run.fabric.py" --json ping
python3 -I "$FABRIC_SERVER_ROOT/run.fabric.py" --json invoke server:getStatus
python3 -I "$FABRIC_SERVER_ROOT/run.fabric.py" --json invoke server:getHealthThe server may take a few seconds to become ready; retry the CLI checks if they initially report a connection failure. Status should report the installed Fabric version, and health should report ok. Create a workspace once if this is a new server:
python3 -I "$FABRIC_SERVER_ROOT/run.fabric.py" --json invoke server:createWorkspace '"Server"'For start at login, run systemctl --user enable fabric-agent-server.service. For operation after logout or across reboots, ask the host administrator to enable user lingering with loginctl enable-linger YOUR_USER. Check your host's firewall and service policy before changing either setting.
Connect securely
Keep the server bound to 127.0.0.1. For SSH, run this on your desktop, replacing fabric-server with your configured SSH host alias. Verify the host key through a trusted channel on the first connection.
ssh -N -o ExitOnForwardFailure=yes -o ServerAliveInterval=30 \
-L 127.0.0.1:19100:127.0.0.1:9100 fabric-serverKeep that terminal open. Visit http://127.0.0.1:19100 in a browser, or choose Add Workspace → Connect Remote in Fabric Agents and use ws://127.0.0.1:19100. Enter the server token, then choose the remote workspace.
To copy the token into a Wayland desktop's clipboard without printing it, run this on that desktop, using the same SSH alias:
ssh fabric-server python3 - <<'PY' | wl-copy
import json
from pathlib import Path
file = Path.home() / '.local/share/fabric-agent-server/server.env'
for line in file.read_text().splitlines():
key, value = line.split('=', 1)
if key == 'FABRIC_SERVER_TOKEN':
print(json.loads(value), end='')
break
else:
raise SystemExit('Token not found')
PYOn macOS, replace wl-copy with pbcopy. If you changed the profile location, adjust the remote path. Clear the token field before pasting. Keep tokens out of URLs, command arguments and shared logs.
For an HTTPS endpoint, keep the same loopback bind and use a trusted TLS reverse proxy, such as Tailscale Serve. The browser endpoint is https://...; the desktop endpoint is wss://.... Set FABRIC_WEBUI_SECURE_COOKIE="true" and FABRIC_WEBUI_WS_URL="wss://YOUR_HOST" in the private environment file and add both names to the launcher's allowed-key tuple, then restart the service. Protect the proxy with tailnet access controls. A shared HTTPS endpoint still requires the Fabric server token. Plain WebSocket connections are accepted only for loopback; do not expose port 9100 publicly or disable certificate validation.
Sign in on the server
Open the server's browser UI through the secure connection and complete provider sign-in there. Sign in to sources on that host as needed. Provider credentials are encrypted in this server's private profile. A provider CLI can have a separate sign-in from Fabric's browser setup.
Never copy .claude, another Fabric profile, environment secret files or provider tokens from your laptop. An authenticated connection does not prove that the account has access to a particular model or sufficient capacity. Verify one conversation in a disposable working folder before using important projects.
Tools execute on the server and use its isolated home. Configure Git and other tools there deliberately. Browser tools run on the connected desktop. Machines settings, fleet management and delegation are still in development.
Update and roll back
- Download and verify the new release using the download commands, then run the extraction command with the new version. Keep the previous version directory. Do not run the fresh-profile creation command again.
- Finish active sessions before maintenance. Query your server with
python3 -I "$FABRIC_SERVER_ROOT/run.fabric.py" --json invoke server:getActiveSessionsand confirm no work is processing. - Stop only this service:
systemctl --user stop fabric-agent-server.service. Make a mode-600 backup ofserver.env. Preserve the token, home, XDG paths,FABRIC_CONFIG_DIRand provider credential store. Inserver.env, change paths pointing at the oldapp/VERSIONto the new directory, and changeFABRIC_VERSION. Update the unit'sWorkingDirectoryand theapp/currentsymlink to the new version. - Run
systemctl --user daemon-reload, start this service, and repeat the version and health checks. Confirm the same workspaces and provider connection, then test a conversation. Update the desktop to the matching release. Hard-reload the browser to refresh icons. - If verification fails, stop this service, restore the previous private environment file, unit working directory and
app/currenttarget, then reload and restart. This rolls back the runtime; it is not a backup of conversations or provider credentials. Review any data-migration requirements in release notes before updating.
Keep private backups private. Do not regenerate the token during an ordinary update. Retain the previous verified runtime until the new one passes your checks.
Run the shell commands in the same Bash session, or re-export the release and profile variables when you open a new session. The active-session check must return [] before stopping the service. If download or extraction fails, inspect the new version's partial files before removing them and retrying; never remove the current profile or its data.
Troubleshooting
- Invalid credentials: clear the input and copy the token from this profile's current environment file again. Confirm the tunnel points at this server.
- Connection refused: check the user service and forwarding port; a successful SSH login alone does not create a tunnel.
- Old icon: hard-reload, check the running version and
FABRIC_WEBUI_DIR, and verify you are connected to the intended server. - Missing runtime assets: re-check the archive checksum and paths. Use the server archive, which includes server resources. Do not install dependencies inside the extracted release to repair a mismatched artifact.
- Provider error: complete sign-in on this server and check the account's model access and quota. Keep credentials and unredacted environment files out of support messages.
Workspaces
How workspaces isolate configurations in Fabric Agents — creating, switching, deleting, per-workspace vs global settings, and remote (thin-client) workspaces.
Sharing
Publish a Fabric Agents session to a public read-only URL. One-click share, revoke, what's included in the payload, and where the data lives.