Cb Security Hardening
BlkLeg/CircuitBreaker
Enforces Circuit Breaker security hardening conventions across backend, frontend, Docker, and nginx.
Invoke on ANY Francis task — routes, WebSocket, SSE, streaming, real-time, chat, Plug middleware, auth, CORS, static assets, deploy, Dockerfile, JSON API, uploads, sessions, use Francis, ws/2…
$ npx skills add francis-build/francis --skill francis-thinking -a claude-codeProject install by default; add -g for ~/.claude/skills/.
$ gh skill install francis-build/francis francis-thinking --agent claude-codeProject scope by default; add --scope user for a personal install. Needs GitHub CLI 2.90.0 or later (public preview).
$ git clone --depth 1 https://github.com/francis-build/francis.git skills-src && mkdir -p .claude/skills && cp -r skills-src/skills/francis-thinking .claude/skills/francis-thinking && rm -rf skills-srcUse ~/.claude/skills/ instead of .claude/skills for a personal install. The folder must contain SKILL.md.
Claude Code skills documentation · loads skills from .claude/skills/
Install the "francis-thinking" agent skill from https://github.com/francis-build/francis/tree/main/skills/francis-thinking into .claude/skills/francis-thinking/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "francis-thinking", then confirm the skill loads.Claude Code copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$skill-installer install https://github.com/francis-build/francis/tree/main/skills/francis-thinkingType this inside Codex. $skill-installer <name> installs a curated skill from openai/skills. The installer writes to $CODEX_HOME/skills (default ~/.codex/skills). Restart Codex if the skill does not show up.
$ npx skills add francis-build/francis --skill francis-thinking -a codexProject install goes to .agents/skills/; add -g for ~/.codex/skills/.
$ gh skill install francis-build/francis francis-thinking --agent codexProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/francis-build/francis.git skills-src && mkdir -p .agents/skills && cp -r skills-src/skills/francis-thinking .agents/skills/francis-thinking && rm -rf skills-srcUse ~/.agents/skills/ instead of .agents/skills for a personal install.
Codex skills documentation · loads skills from .agents/skills/
Install the "francis-thinking" agent skill from https://github.com/francis-build/francis/tree/main/skills/francis-thinking into .agents/skills/francis-thinking/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "francis-thinking", then confirm the skill loads.Codex copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ npx skills add francis-build/francis --skill francis-thinking -a cursorProject install goes to .agents/skills/; add -g for ~/.cursor/skills/.
$ gh skill install francis-build/francis francis-thinking --agent cursorProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/francis-build/francis.git skills-src && mkdir -p .cursor/skills && cp -r skills-src/skills/francis-thinking .cursor/skills/francis-thinking && rm -rf skills-srcUse ~/.cursor/skills/ instead of .cursor/skills for a personal install.
Cursor skills documentation · loads skills from .cursor/skills/, .agents/skills/, .claude/skills/, .codex/skills/
Install the "francis-thinking" agent skill from https://github.com/francis-build/francis/tree/main/skills/francis-thinking into .cursor/skills/francis-thinking/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "francis-thinking", then confirm the skill loads.Cursor copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ gemini skills install https://github.com/francis-build/francis.git --path skills/francis-thinking--scope user (default) or --scope workspace; --path is the subfolder of the repo that holds the skill; --consent skips the security confirmation prompt.
$ npx skills add francis-build/francis --skill francis-thinking -a gemini-cliProject install goes to .agents/skills/; add -g for ~/.gemini/skills/.
$ gh skill install francis-build/francis francis-thinking --agent gemini-cliProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/francis-build/francis.git skills-src && mkdir -p .gemini/skills && cp -r skills-src/skills/francis-thinking .gemini/skills/francis-thinking && rm -rf skills-srcUse ~/.gemini/skills/ instead of .gemini/skills for a personal install, then run /skills reload.
Gemini CLI skills documentation · loads skills from .gemini/skills/, .agents/skills/
Install the "francis-thinking" agent skill from https://github.com/francis-build/francis/tree/main/skills/francis-thinking into .gemini/skills/francis-thinking/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "francis-thinking", then confirm the skill loads.Gemini CLI copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ gh skill install francis-build/francis francis-thinkingInstalls for Copilot at project scope by default; add --scope user for a personal install. Preview a skill first with gh skill preview. Needs GitHub CLI 2.90.0 or later (public preview).
$ npx skills add francis-build/francis --skill francis-thinking -a github-copilotProject install goes to .agents/skills/; add -g for ~/.copilot/skills/.
$ git clone --depth 1 https://github.com/francis-build/francis.git skills-src && mkdir -p .github/skills && cp -r skills-src/skills/francis-thinking .github/skills/francis-thinking && rm -rf skills-srcUse ~/.copilot/skills/ instead of .github/skills for a personal install. Commit .github/skills so cloud agent and code review can use it.
GitHub Copilot skills documentation · loads skills from .github/skills/, .claude/skills/, .agents/skills/
Install the "francis-thinking" agent skill from https://github.com/francis-build/francis/tree/main/skills/francis-thinking into .github/skills/francis-thinking/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "francis-thinking", then confirm the skill loads.GitHub Copilot copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ npx skills add francis-build/francis --skill francis-thinking -a opencodeOpenCode documents no install command of its own. Project install goes to .agents/skills/; add -g for ~/.config/opencode/skills/.
$ gh skill install francis-build/francis francis-thinking --agent opencodeProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/francis-build/francis.git skills-src && mkdir -p .opencode/skills && cp -r skills-src/skills/francis-thinking .opencode/skills/francis-thinking && rm -rf skills-srcUse ~/.config/opencode/skills/ instead of .opencode/skills for a personal install.
OpenCode skills documentation · loads skills from .opencode/skills/, .claude/skills/, .agents/skills/
Install the "francis-thinking" agent skill from https://github.com/francis-build/francis/tree/main/skills/francis-thinking into .opencode/skills/francis-thinking/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "francis-thinking", then confirm the skill loads.OpenCode copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
francis-thinkingInvoke on ANY Francis task — routes, WebSocket, SSE, streaming, real-time, chat, Plug middleware, auth, CORS, static assets, deploy, Dockerfile, JSON API, uploads, sessions, use Francis, ws/2…
Francis Thinking is an agent skill from francis-build/francis. Invoke on ANY Francis task — routes, WebSocket, SSE, streaming, real-time, chat, Plug middleware, auth, CORS, static assets, deploy, Dockerfile, JSON API, uploads, sessions, use Francis, ws/2, sse/2, socket.transport, Francis.Plug, Francis.HTML, Francis.Static, banditopts, or contributing to the framework itself. Contains the unified event model, all API details, gotchas, and red flags.
Its SKILL.md is about 3k tokens, which your agent loads only when the skill is triggered. It is a single SKILL.md file with no bundled scripts.
It sits in Backend & APIs, covering Realtime and WebSockets and Containers. It works with Docker. The repository describes itself as: Boilerplate killer using Bandit and Plug. The licence is MIT.
Read from SKILL.md and the folder at commit 323c88c. It shows what the files ask for, not the result of running them.
Pre-approves nothing: there is no allowed-tools line, so your agent's usual permission prompts apply.
From allowed-tools in the SKILL.md frontmatter.
No scripts in the folder and no shell commands in SKILL.md (its code samples are elixir and bash).
From the folder's file list and the shell code blocks in SKILL.md.
No URLs in SKILL.md.
From URLs in SKILL.md, links to its own repository left out.
Names no API keys, tokens, secrets or passwords.
From names ending in _API_KEY, _TOKEN, _SECRET, _KEY or _PASSWORD in SKILL.md.
Francis Thinking loads about 3k tokens when it runs. Until then it costs about 106 tokens; SKILL.md has 796 words of instructions outside code blocks.
Estimates: characters ÷ 4, the usual rule of thumb; real counts depend on the model's tokenizer. Scripts and assets cost tokens only if the agent reads them.
The automated check found no risky patterns in SKILL.md.
Automated static check — not a guarantee. Review scripts before installing. It scans the text of SKILL.md for risky patterns (piping downloads into a shell, reading credential files, hidden Unicode, destructive commands); files beside SKILL.md are not scanned.
The full file from francis-build/francis at commit 323c88c, republished under its MIT licence (© francis-build). 796 words, ~3,019 tokens.
.claude/skills/francis-thinking/SKILL.md (or your agent's skills folder).<EXTREMELY-IMPORTANT>
Invoke this skill BEFORE doing ANYTHING else on a Francis task — including exploring the codebase.
Francis macros generate hidden modules at compile time. Exploring first means you won't know what to look for, and you'll miss critical safety rules (HTML escaping, SSE directionality, redirect safety).
</EXTREMELY-IMPORTANT>
Francis task → Read this skill FIRST → Then explore → Then write codeEven "where is the route defined?" needs this skill first — get/ws/sse macros generate hidden modules; grepping for the handler won't find them.
# WRONG — html/2 does NOT escape. XSS vulnerability:
html(conn, "<p>Hello #{user_input}</p>")
# WRONG — safe_html/2 escapes the ENTIRE string, including your <p> tags:
safe_html(conn, "<p>Hello #{user_input}</p>")
# renders: <p>Hello user input</p> — raw text, not HTML
# RIGHT — escape only the interpolation, keep your trusted markup:
html(conn, "<p>Hello #{Francis.HTML.escape(user_input)}</p>")
# RIGHT — safe_html/2 is for rendering untrusted content as escaped plain text:
safe_html(conn, user_input)All three transports share the same shape:
HTTP: fn conn -> response
WS/SSE: fn event, socket -> replyReturn value dispatch (HTTP handlers):
| Return value | What Francis sends |
|---|---|
| Binary string | 200, no content-type set (use text/2 to force text/plain) |
| Map or list | 200 JSON (application/json) |
Plug.Conn struct | Sent as-is (full control) |
{:error, reason} | Calls error handler |
Status is 200 for route macros, 404 for unmatched/1. Return Plug.Conn to override either.
Reply value dispatch (WS/SSE handlers):
| Return value | What Francis sends |
|---|---|
{:reply, binary} | Text frame / SSE data: line |
{:reply, map | list} | JSON-encoded text frame / SSE data: line |
{:reply, {type, payload}} | Typed WS frame: type in :text, :binary, :ping, :pong |
:noreply or :ok | Nothing sent |
{:reply, {:binary, bytes}} is the only way to send binary WebSocket frames.
defmodule MyApp do
use Francis
get("/", fn _conn -> "hello" end)
get("/users/:id", fn conn -> "user #{conn.params["id"]}" end)
post("/users", fn conn -> conn.body_params end)
put("/users/:id", fn conn -> %{updated: conn.params["id"]} end)
delete("/users/:id", fn conn -> %{deleted: conn.params["id"]} end)
patch("/users/:id", fn conn -> conn.body_params end)
unmatched(fn _conn -> "not found" end)
endunmatched/1 must be declared last — it shadows any routes declared after it.
Accessing data:
conn.params["id"]conn.body_params — requires a matching content-type header (application/json, application/x-www-form-urlencoded, multipart/form-data). Without it, body_params is %{}. Body params are also merged into conn.params after parsing.Response helpers (auto-imported via Francis.ResponseHandlers):
json(conn, %{ok: true})
json(conn, 201, %{id: 1})
text(conn, "hello")
html(conn, "<h1>Trusted static HTML only</h1>")
safe_html(conn, user_input) # escapes the whole string as plain text
safe_html(conn, 201, user_input)
redirect(conn, "/new") # relative paths only
redirect(conn, 301, "/new")HEAD requests — no head/2 macro. Plug.Head (installed by default) converts HEAD requests to GET automatically.
ws("/chat/:room", fn
:join, socket ->
{:reply, %{type: "welcome", room: socket.params["room"]}}
{:received, msg}, socket ->
{:reply, "[#{socket.params["room"]}] #{msg}"}
{:close, _reason}, _socket ->
:ok
end)
ws("/live", handler_fn, heartbeat_interval: 10_000, timeout: 120_000)Events:
:join — client connected{:received, message} — client sent a text message over the wire{:close, reason} — connection closed:join and {:close, _} are optional — succeed silently if unmatched. Easy to lose cleanup logic on close.
Socket state:
%{
id: "64-character hex string (32 random bytes)",
transport: pid,
path: "/chat/general",
params: %{"room" => "general"}
}send(socket.transport, msg) for WS bypasses the handler entirely. Messages are forwarded directly to the client. They do NOT pass through your {:received, _} clause. Store socket.transport to broadcast from other processes.
Options:
:heartbeat_interval (default: 30_000 ms) — ping frames; nil to disable:timeout (default: 60_000 ms) — idle connection timeout:max_frame_size (default: 65_536 bytes) — memory protectionModule name collision: each ws/3 call generates a module named from the route path. Two routes with structurally identical paths generate the same module name and silently overwrite each other.
SSE is server→client only. The SSE client has no upstream channel. All events the handler receives come from other processes via send(socket.transport, msg).
sse("/events", fn
:join, socket ->
{:reply, %{event: "connected", data: %{id: socket.id}}}
{:received, msg}, socket ->
{:reply, msg}
{:close, _reason}, _socket ->
:ok
end)
sse("/stream", handler_fn, keepalive_interval: 30_000)send(socket.transport, msg) for SSE routes through the handler's {:received, msg} clause — unlike WS where it bypasses the handler. You can transform or filter messages before they reach the client.
Real close reasons include :chunk_failed (client disconnected) and :keepalive_failed. Use {:close, _} to clean up subscriptions.
SSE event formats:
{:reply, "plain text"}
# => data: plain text\n\n
{:reply, %{status: "ok"}}
# => data: {"status":"ok"}\n\n
{:reply, %{event: "user_joined", data: %{name: "Alice"}, id: "42", retry: 5000}}
# => event: user_joined\ndata: {...}\nid: 42\nretry: 5000\n\nOptions:
:keepalive_interval (default: 15_000 ms) — comment line to keep connection alive; nil to disableWS vs SSE — critical difference:
WS send(socket.transport, msg) | SSE send(socket.transport, msg) | |
|---|---|---|
| Routes through handler? | No — sent directly to client | Yes — delivered to {:received, msg} |
{:received, _} source | Client text frames over the wire | Other processes only |
Plugs run before route handlers, in declaration order. Auth plugs must come before route macros.
defmodule MyApp do
use Francis
import Plug.BasicAuth
plug Francis.Plug.SecureHeaders
plug Francis.Plug.CSP
plug :basic_auth, username: "admin", password: "secret"
get("/", fn _ -> "authenticated" end)
endRouter forwarding for scoped middleware:
defmodule Public do
use Francis
get("/", fn _ -> "public" end)
end
defmodule Private do
use Francis
import Plug.BasicAuth
plug :basic_auth, username: "admin", password: "secret"
get("/", fn _ -> "private" end)
end
defmodule Main do
use Francis
forward("/public", to: Public)
forward("/private", to: Private)
unmatched(fn _ -> "not found" end)
endforward/2 and plug/1-2 are from Plug.Router/Plug.Builder — see Plug docs for full options.
plug Francis.Plug.SecureHeaders
plug Francis.Plug.SecureHeaders, headers: %{"x-frame-options" => "SAMEORIGIN"}
plug Francis.Plug.CSP
plug Francis.Plug.CSP,
directives: %{"script-src" => "'self' https://cdn.example.com"},
report_only: trueredirect/2 and redirect/3 accept relative paths only. Absolute URLs raise ArgumentError. Protocol-relative URLs (//evil.com) are converted to /.
defmodule MyApp do
use Francis, error_handler: &__MODULE__.handle_error/2
get("/risky", fn _ -> {:error, :unavailable} end)
def handle_error(conn, {:error, :unavailable}),
do: Plug.Conn.send_resp(conn, 503, "Service unavailable")
def handle_error(conn, _),
do: Plug.Conn.send_resp(conn, 500, "Internal error")
endThe error handler receives both {:error, reason} tuples and raised exceptions. If the error handler itself raises, Francis catches it and renders the default 500 page.
Keys valid in both use Francis opts and config.exs: bandit_opts, static, log_level, error_handler, parser.
dev: true is only read from config.exs, never from use Francis opts. If both locations set the same key, use opts win and a warning is logged.
config :francis,
bandit_opts: [port: 4000],
static: [from: "priv/static", at: "/"],
parser: [parsers: [:json, :urlencoded, :multipart], json_decoder: Jason],
error_handler: &MyApp.Errors.handle/2,
log_level: :info,
dev: trueNote: the outer key is singular :parser; the inner Plug.Parsers key is plural :parsers.
use Francis, static: [from: "priv/static", at: "/"]mix francis.digest # hash all assets, write cache_manifest.json
mix francis.digest --clean # remove old digested files, then re-digest
mix francis.digest --gzip false
mix francis.digest --exclude '*.json'
mix francis.digest --age 86400 # cache-control max-age in seconds (default: 31536000)Francis.Static.static_path("app.css") # => "/app-a1b2c3d4.css"mix francis.server
iex -S mix francis.server
mix francis.new my_app
mix francis.new my_app --sup
mix francis.new my_app --sup MyApp
mix francis.release --port 8080 --elixir-version 1.18.4 --otp-version 27.3.4defmodule MyAppTest do
use ExUnit.Case, async: true
use Plug.Test
@opts MyApp.init([])
test "GET /" do
conn = conn(:get, "/") |> MyApp.call(@opts)
assert conn.status == 200
assert conn.resp_body == "hello"
end
test "POST /users" do
conn =
conn(:post, "/users", Jason.encode!(%{name: "Alice"}))
|> put_req_header("content-type", "application/json")
|> MyApp.call(@opts)
assert conn.status == 201
assert %{"name" => "Alice"} = Jason.decode!(conn.resp_body)
end
endAlways prefix mix commands with unbuffer: unbuffer mix test
| Situation | Correct approach |
|---|---|
| Rendering user input in HTML | html(conn, "<p>#{Francis.HTML.escape(input)}</p>") |
| Rendering untrusted text | safe_html(conn, input) — escapes entire string; do NOT wrap in markup |
| SSE pushing from handler | SSE is server→client; push via send(socket.transport, msg) from another process |
| WS broadcasting from another process | send(socket.transport, msg) bypasses handler — goes direct to client |
Forgetting :close handler | Silent success — add {:close, _} to clean up subscriptions and ETS entries |
Absolute URL in redirect | Francis raises ArgumentError — relative paths only |
dev: true not working | Only valid in config.exs, not in use Francis opts |
| Auth inside a route handler | Move to a plug before routes, or scope with forward/2 |
body_params is %{} | Caller must send a matching content-type header |
| Two ws/sse routes with same path shape | Generate the same module name — silently overwrite each other |
unmatched/1 not catching routes | Must be declared last — shadows everything after it |
© francis-build, MIT. Rendered from Markdown: HTML in the file is shown as text, images as links, and headings moved down two levels. Raw file
Just SKILL.md in skills/francis-thinking of francis-build/francis.
Open the folder on GitHubat commit 323c88c
Francis Thinking next to the 5 skills that share the most tags, products or categories with it. Stars are the repository's; “used in” counts other GitHub owners with a copy.
| Skill | Stars | Used in | Tokens | Auto-check | Licence | Repo updated |
|---|---|---|---|---|---|---|
| Francis Thinking this skillfrancis-build/francis | 107 | — | ~3k | Automated safety check: Pass | MIT | |
| Cb Security HardeningBlkLeg/CircuitBreaker | 201 | — | ~2.1k | Automated safety check: Pass | MIT | |
| Openenv Agentic Rlburtenshaw/training-agents | 153 | — | ~381 | Automated safety check: Pass | Apache-2.0 | |
| Vercel Functionsvercel/vercel-plugin | 301 | — | ~12k | Automated safety check: Notes | Custom licence | |
| Tgf Server Devthkhxm/tgf | 128 | — | ~1.3k | Automated safety check: Notes | MIT | |
| Acarshub Socket Namespacesdr-enthusiasts/docker-acarshub | 117 | — | ~710 | Automated safety check: Pass | GPL-3.0 |
BlkLeg/CircuitBreaker
Enforces Circuit Breaker security hardening conventions across backend, frontend, Docker, and nginx.
burtenshaw/training-agents
A skill your agent uses when designing, reviewing, or implementing OpenEnv-style environment interfaces for agentic RL with TRL, including reset/step/state contracts, tasksets, Docker or…
vercel/vercel-plugin
Vercel Functions expert guidance — Node.js/Bun/Python runtimes, Fluid Compute, long-duration (30 min) functions, large functions (5 GB bundles), Docker/OCI container images, plan limits, streaming…
thkhxm/tgf
基于 tgf v2(github.com/thkhxm/tgf/v2)用确定性的 tgfctl 工作流创建、验证和维护 Go 游戏服务器项目。
sdr-enthusiasts/docker-acarshub
Use ONLY when working in the docker-acarshub repository AND touching socket.io code -- emit / on / connect calls on either the React frontend or the Fastify backend.
NangoHQ/nango
A skill your agent uses when running the Nango application locally for development and browser testing - covers Docker services, dev commands, service URLs, and troubleshooting startup issues
Works with
Categories
Invoke on ANY Francis task — routes, WebSocket, SSE, streaming, real-time, chat, Plug middleware, auth, CORS, static assets, deploy, Dockerfile, JSON API, uploads, sessions, use Francis, ws/2…. Francis Thinking is an agent skill from francis-build/francis.Static, banditopts, or contributing to the framework itself.
Francis Thinking fits situations like: tasks that involve Realtime and WebSockets; tasks that involve Containers.
Run `npx skills add francis-build/francis --skill francis-thinking -a claude-code`. Or copy the skill folder (skills/francis-thinking in francis-build/francis) into .claude/skills/francis-thinking in your project. Claude Code loads it when a task matches its description.
Run `npx skills add francis-build/francis --skill francis-thinking -a codex`. Or copy the skill folder (skills/francis-thinking in francis-build/francis) into .agents/skills/francis-thinking in your project. Codex loads it when a task matches its description.
Cursor, Gemini CLI, GitHub Copilot and OpenCode also load SKILL.md folders. With the skills CLI, run `npx skills add francis-build/francis --skill francis-thinking -a cursor` (or -a gemini-cli, github-copilot or opencode for the others). To copy it by hand, put the folder in .cursor/skills/francis-thinking, .gemini/skills/francis-thinking, .github/skills/francis-thinking and .opencode/skills/francis-thinking in your project.
SKILL.md names no scripts, command-line tools or credentials: Francis Thinking is instructions for the agent only.
SKILL.md contains no URLs. Any network use would come from the scripts or tools the agent runs. This is read from the text; nothing was executed.
Our automated static check of SKILL.md found no risky patterns, such as piping downloads into a shell, reading credential files or hidden Unicode. It is not a guarantee. Review the folder before installing.
Francis Thinking is published under the MIT licence (declared in SKILL.md). It allows redistribution, so the full SKILL.md is shown on this page.
About 3k tokens (SKILL.md is roughly 12k characters). Agents keep only the skill's name and description in context until a task matches; then they load SKILL.md in full.
Skills that share tags, products or a category with Francis Thinking: Cb Security Hardening (BlkLeg/CircuitBreaker, 201 stars), Openenv Agentic Rl (burtenshaw/training-agents, 153 stars), Vercel Functions (vercel/vercel-plugin, 301 stars) and Tgf Server Dev (thkhxm/tgf, 128 stars). The comparison table on this page puts their stars, adoption, token cost, safety result and licence side by side.
francis-build (a GitHub organization) maintains it in francis-build/francis, which has 107 GitHub stars. The repository was last updated on August 26, 2026.
Source: francis-build/francis on GitHub. Facts on this page come from the repository at the commit we read; the author's words are quoted as theirs.