Skip to content
Open
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
20 changes: 18 additions & 2 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -97,8 +97,17 @@ HTTP_WRITE_TIMEOUT=60
HTTP_CONNECT_TIMEOUT=60


# Optional server API key (Anthropic-style)
ANTHROPIC_AUTH_TOKEN="freecc"
# Server binding. Keep HOST on loopback unless you intentionally expose the
# proxy and have configured a strong ANTHROPIC_AUTH_TOKEN.
HOST="127.0.0.1"
PORT=8082


# Optional server API key (Anthropic-style).
# fcc-init writes a random value here. If copying this template manually, set
# a unique local secret and use the same value in your Claude Code environment.
# Leave empty only for trusted loopback-only testing.
ANTHROPIC_AUTH_TOKEN=""


# Messaging Platform: "telegram" | "discord" | "none"
Expand All @@ -120,6 +129,10 @@ WHISPER_DEVICE="nvidia_nim"
# - For nvidia_nim, default to "openai/whisper-large-v3" for best performance
WHISPER_MODEL="openai/whisper-large-v3"
HF_TOKEN=""
# Optional pinned Hugging Face revision for custom cpu/cuda Whisper models.
# Built-in short names (tiny/base/small/medium/large-v2/large-v3/large-v3-turbo)
# are pinned by the application.
HF_MODEL_REVISION=""


# Telegram Config
Expand All @@ -136,6 +149,9 @@ ALLOWED_DISCORD_CHANNELS=""
CLAUDE_WORKSPACE=
ALLOWED_DIR=""
CLAUDE_CLI_BIN="claude"
# Set true only in an isolated workspace where Claude Code may run without
# interactive tool permission prompts.
CLAUDE_CLI_SKIP_PERMISSIONS=false
FAST_PREFIX_DETECTION=true
ENABLE_NETWORK_PROBE_MOCK=true
ENABLE_TITLE_GENERATION_SKIP=true
Expand Down
27 changes: 22 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,16 @@ uv tool install --force git+https://github.com/Alishahryar1/free-claude-code.git

Use the same command to update to the latest version.

Optional file-based setup:

```bash
fcc-init
```

`fcc-init` creates `~/.fcc/.env` from the bundled template and writes a fresh random `ANTHROPIC_AUTH_TOKEN`. You can also configure the proxy from the Admin UI after startup.

Use a unique local secret for `ANTHROPIC_AUTH_TOKEN`; Claude Code sends the same value back to this proxy. Leave it empty only for trusted loopback-only testing.

### 5. Start The Proxy

```bash
Expand All @@ -103,6 +113,8 @@ Admin UI: http://127.0.0.1:8082/admin

Many terminals make these clickable. Use your configured `PORT` if it is not `8082`.

By default the proxy binds to `127.0.0.1`. Bind to `0.0.0.0` only when the proxy must be reachable from another host, and keep `ANTHROPIC_AUTH_TOKEN` set to a strong secret in that case.

### 6. Open The Admin UI And Configure NVIDIA NIM

Open the **Admin UI** URL from the terminal output.
Expand Down Expand Up @@ -267,7 +279,7 @@ Open Settings, search for `claude-code.environmentVariables`, choose **Edit in s
```json
"claudeCode.environmentVariables": [
{ "name": "ANTHROPIC_BASE_URL", "value": "http://localhost:8082" },
{ "name": "ANTHROPIC_AUTH_TOKEN", "value": "freecc" },
{ "name": "ANTHROPIC_AUTH_TOKEN", "value": "<same-secret-from-.env>" },
{ "name": "CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY", "value": "1" }
]
```
Expand All @@ -286,7 +298,7 @@ Set the environment for `acp.registry.claude-acp`:
```json
"env": {
"ANTHROPIC_BASE_URL": "http://localhost:8082",
"ANTHROPIC_AUTH_TOKEN": "freecc",
"ANTHROPIC_AUTH_TOKEN": "<same-secret-from-.env>",
"CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY": "1"
}
```
Expand Down Expand Up @@ -329,6 +341,8 @@ ALLOWED_DIR="C:/Users/yourname/projects"

Get a token from [@BotFather](https://t.me/BotFather) and your user ID from [@userinfobot](https://t.me/userinfobot).

Set `CLAUDE_CLI_SKIP_PERMISSIONS=true` only for an isolated workspace where Claude Code may run tools without interactive permission prompts.

Useful commands:

- `/stop` cancels a task; reply to a task message to stop only that branch.
Expand All @@ -350,9 +364,10 @@ VOICE_NOTE_ENABLED=true
WHISPER_DEVICE="cpu" # cpu | cuda | nvidia_nim
WHISPER_MODEL="base"
HF_TOKEN=""
HF_MODEL_REVISION="" # required for custom Hugging Face local models
```

Use `WHISPER_DEVICE="nvidia_nim"` with the `voice` extra and `NVIDIA_NIM_API_KEY` for NVIDIA-hosted transcription.
Built-in short local model names are pinned to immutable Hugging Face revisions. Use `HF_MODEL_REVISION` when `WHISPER_MODEL` is a custom Hugging Face model ID. Use `WHISPER_DEVICE="nvidia_nim"` with the `voice` extra and `NVIDIA_NIM_API_KEY` for NVIDIA-hosted transcription.

## Configuration Reference

Expand All @@ -371,7 +386,7 @@ Example for NVIDIA NIM:
```dotenv
NVIDIA_NIM_API_KEY="nvapi-your-key"
MODEL="nvidia_nim/z-ai/glm4.7"
ANTHROPIC_AUTH_TOKEN="freecc"
ANTHROPIC_AUTH_TOKEN="<random-local-secret>"
```

Config precedence is repo `.env`, then `~/.fcc/.env`, then `FCC_ENV_FILE` when set. Blank `CLAUDE_WORKSPACE` uses `~/.fcc/agent_workspace`. `ANTHROPIC_AUTH_TOKEN` can be any local secret; pass the same value to Claude Code.
Expand Down Expand Up @@ -433,7 +448,9 @@ Use lower limits for free hosted providers; local providers can usually tolerate
### 5. Security And Diagnostics

```dotenv
ANTHROPIC_AUTH_TOKEN=
HOST="127.0.0.1"
PORT=8082
ANTHROPIC_AUTH_TOKEN="<random-local-secret>"
LOG_RAW_API_PAYLOADS=false
LOG_RAW_SSE_EVENTS=false
LOG_API_ERROR_TRACEBACKS=false
Expand Down
21 changes: 19 additions & 2 deletions api/admin_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -344,7 +344,6 @@ class ConfigFieldSpec:
"runtime",
"secret",
settings_attr="anthropic_auth_token",
default="freecc",
secret=True,
description="Protects Claude/API access. It is not admin-page login.",
),
Expand Down Expand Up @@ -401,7 +400,7 @@ class ConfigFieldSpec:
"Server Host",
"runtime",
settings_attr="host",
default="0.0.0.0",
default="127.0.0.1",
restart_required=True,
),
ConfigFieldSpec(
Expand Down Expand Up @@ -497,6 +496,16 @@ class ConfigFieldSpec:
default="claude",
session_sensitive=True,
),
ConfigFieldSpec(
"CLAUDE_CLI_SKIP_PERMISSIONS",
"Skip Claude CLI Permissions",
"messaging",
"boolean",
settings_attr="claude_cli_skip_permissions",
default="false",
session_sensitive=True,
description="Adds --dangerously-skip-permissions only when explicitly enabled.",
),
ConfigFieldSpec(
"MAX_MESSAGE_LOG_ENTRIES_PER_CHAT",
"Max Message Log Entries",
Expand Down Expand Up @@ -542,6 +551,14 @@ class ConfigFieldSpec:
secret=True,
session_sensitive=True,
),
ConfigFieldSpec(
"HF_MODEL_REVISION",
"Hugging Face Model Revision",
"voice",
settings_attr="hf_model_revision",
session_sensitive=True,
description="Required for custom Hugging Face local Whisper models.",
),
ConfigFieldSpec(
"FAST_PREFIX_DETECTION",
"Fast Prefix Detection",
Expand Down
2 changes: 1 addition & 1 deletion api/admin_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,7 @@ def _next_admin_url() -> str:
field["key"]: field["value"] for field in load_config_response()["fields"]
}
settings = Settings.model_construct(
host=fields.get("HOST") or "0.0.0.0",
host=fields.get("HOST") or "127.0.0.1",
port=int(fields.get("PORT") or 8082),
)
return local_admin_url(settings)
Expand Down
10 changes: 9 additions & 1 deletion api/admin_urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,22 @@

from __future__ import annotations

import ipaddress

from config.settings import Settings


def _browser_host_for_local_urls(settings: Settings) -> str:
"""Host fragment for URLs shown to humans on the same machine as the server."""

host = settings.host.strip() if settings.host else "127.0.0.1"
if host in {"0.0.0.0", "::", "[::]"}:
normalized = host[1:-1] if host.startswith("[") and host.endswith("]") else host
try:
if ipaddress.ip_address(normalized).is_unspecified:
host = "127.0.0.1"
except ValueError:
pass
if host.lower() == "localhost":
host = "127.0.0.1"
if ":" in host and not host.startswith("["):
host = f"[{host}]"
Expand Down
2 changes: 2 additions & 0 deletions api/runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,7 @@ async def _start_messaging_if_configured(self) -> None:
whisper_model=self.settings.whisper_model,
whisper_device=self.settings.whisper_device,
hf_token=self.settings.hf_token,
hf_model_revision=self.settings.hf_model_revision,
nvidia_nim_api_key=self.settings.nvidia_nim_api_key,
messaging_rate_limit=self.settings.messaging_rate_limit,
messaging_rate_window=self.settings.messaging_rate_window,
Expand Down Expand Up @@ -256,6 +257,7 @@ async def _start_message_handler(self) -> None:
plans_directory=plans_directory,
claude_bin=self.settings.claude_cli_bin,
auth_token=getattr(self.settings, "anthropic_auth_token", ""),
skip_permissions=self.settings.claude_cli_skip_permissions,
log_raw_cli_diagnostics=self.settings.log_raw_cli_diagnostics,
log_messaging_error_details=self.settings.log_messaging_error_details,
)
Expand Down
56 changes: 45 additions & 11 deletions cli/entrypoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@
from __future__ import annotations

import os
import secrets
import shutil
import subprocess
import sys
from collections.abc import Mapping, Sequence
from http.client import HTTPConnection, HTTPException
from pathlib import Path
from urllib.error import HTTPError, URLError
from urllib.request import Request, urlopen
from urllib.parse import urlsplit

import uvicorn

Expand All @@ -28,6 +29,9 @@
PROXY_PREFLIGHT_TIMEOUT_SECONDS = 1.5
SERVER_GRACEFUL_SHUTDOWN_SECONDS = 5

_AUTH_TOKEN_ENV = "ANTHROPIC_AUTH_TOKEN"
_AUTH_TOKEN_BYTES = 32


def _load_env_template() -> str:
"""Load the canonical root env template from package resources or source."""
Expand All @@ -44,6 +48,22 @@ def _load_env_template() -> str:
raise FileNotFoundError("Could not find bundled or source .env.example template.")


def _with_generated_auth_token(template: str) -> str:
"""Return an env file template with a fresh proxy auth token."""
token_line = f'{_AUTH_TOKEN_ENV}="{secrets.token_urlsafe(_AUTH_TOKEN_BYTES)}"'
prefix = f"{_AUTH_TOKEN_ENV}="
lines = template.splitlines(keepends=True)
for index, line in enumerate(lines):
if line.startswith(prefix):
stripped = line.rstrip("\r\n")
line_ending = line[len(stripped) :]
lines[index] = f"{token_line}{line_ending}"
return "".join(lines)

suffix = "" if template.endswith("\n") or not template else "\n"
return f"{template}{suffix}{token_line}\n"


def serve() -> None:
"""Start the FastAPI server (registered as `fcc-server` script)."""
try:
Expand Down Expand Up @@ -108,7 +128,7 @@ def init() -> None:

config_dir.mkdir(parents=True, exist_ok=True)
template = _load_env_template()
env_file.write_text(template, encoding="utf-8")
env_file.write_text(_with_generated_auth_token(template), encoding="utf-8")
print(f"Config created at {env_file}")
print("Edit it to set your API keys and model preferences, then run: fcc-server")

Expand Down Expand Up @@ -153,16 +173,30 @@ def _preflight_proxy(proxy_root_url: str) -> str | None:
"""Return an error message when the local proxy health check is unreachable."""

url = f"{proxy_root_url.rstrip('/')}{PROXY_PREFLIGHT_PATH}"
request = Request(url, method="GET")
parsed = urlsplit(url)
if parsed.scheme != "http":
return f"unsupported proxy URL scheme {parsed.scheme!r}"
if not parsed.hostname:
return "missing proxy URL host"

target = parsed.path or "/"
if parsed.query:
target = f"{target}?{parsed.query}"

connection = HTTPConnection(
parsed.hostname,
parsed.port or 80,
timeout=PROXY_PREFLIGHT_TIMEOUT_SECONDS,
)
try:
with urlopen(request, timeout=PROXY_PREFLIGHT_TIMEOUT_SECONDS) as response:
status_code = response.getcode()
except HTTPError as exc:
return f"returned HTTP {exc.code}"
except URLError as exc:
return str(exc.reason)
except OSError as exc:
connection.request("GET", target)
response = connection.getresponse()
status_code = response.status
response.read()
except (HTTPException, OSError) as exc:
return str(exc)
finally:
connection.close()

if not 200 <= status_code < 300:
return f"returned HTTP {status_code}"
Expand Down
3 changes: 3 additions & 0 deletions cli/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ def __init__(
claude_bin: str = "claude",
auth_token: str = "",
*,
skip_permissions: bool = False,
log_raw_cli_diagnostics: bool = False,
log_messaging_error_details: bool = False,
):
Expand All @@ -49,6 +50,7 @@ def __init__(
self.plans_directory = plans_directory
self.claude_bin = claude_bin
self.auth_token = auth_token
self.skip_permissions = skip_permissions
self._log_raw_cli_diagnostics = log_raw_cli_diagnostics
self._log_messaging_error_details = log_messaging_error_details

Expand Down Expand Up @@ -85,6 +87,7 @@ async def get_or_create_session(
plans_directory=self.plans_directory,
claude_bin=self.claude_bin,
auth_token=self.auth_token,
skip_permissions=self.skip_permissions,
log_raw_cli_diagnostics=self._log_raw_cli_diagnostics,
)
self._pending_sessions[temp_id] = new_session
Expand Down
Loading