Agent skill

Francis Thinking

by francis-build in 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…

MITAuto-check passedBackend & APIs

Install Francis Thinking

skills CLI
$ npx skills add francis-build/francis --skill francis-thinking -a claude-code

Project install by default; add -g for ~/.claude/skills/.

GitHub CLI
$ gh skill install francis-build/francis francis-thinking --agent claude-code

Project scope by default; add --scope user for a personal install. Needs GitHub CLI 2.90.0 or later (public preview).

Manual copy
$ 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-src

Use ~/.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/

Facts

Skill name
francis-thinking
GitHub stars
107
Token cost
~3k tokens
SKILL.md length
796 words
Files
1
Skills in repo
1
Repo updated
First seen
Licence
MIT

At a glance

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…

  • Tasks that involve Realtime and WebSockets
  • SKILL.md covers The Rule, STOP: The One Footgun That…, The Unified Event Model and HTTP Routes, plus 10 more sections
  • Instructions only: no scripts, shell commands, URLs or credentials in SKILL.md
  • Tasks that involve Containers

What it does

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.

When your agent uses it

  • Tasks that involve Realtime and WebSockets
  • Tasks that involve Containers

Example prompts

  • “/francis-thinking”

What it can do on your machine

Read from SKILL.md and the folder at commit 323c88c. It shows what the files ask for, not the result of running them.

  • Tool permissions

    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.

  • Runs code

    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.

  • Network

    No URLs in SKILL.md.

    From URLs in SKILL.md, links to its own repository left out.

  • Credentials

    Names no API keys, tokens, secrets or passwords.

    From names ending in _API_KEY, _TOKEN, _SECRET, _KEY or _PASSWORD in SKILL.md.

Context cost

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.

Always · name and description, kept in context so the agent knows when to use it
~106
When it runs · the whole SKILL.md, loaded when a task matches
~3k

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.

Safety

Auto-check passed

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.

SKILL.md

The full file from francis-build/francis at commit 323c88c, republished under its MIT licence (© francis-build). 796 words, ~3,019 tokens.

Download SKILL.mdSave it as .claude/skills/francis-thinking/SKILL.md (or your agent's skills folder).
name
francis-thinking
description
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`, `bandit_opts`, or contributing to the framework itself. Contains the unified event model, all API details, gotchas, and red flags.
license
MIT
metadata.author
francis-build
metadata.version
1.0.0
<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>

The Rule

Francis task → Read this skill FIRST → Then explore → Then write code

Even "where is the route defined?" needs this skill first — get/ws/sse macros generate hidden modules; grepping for the handler won't find them.


STOP: The One Footgun That Causes XSS

elixir
# 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: &lt;p&gt;Hello user input&lt;/p&gt;  — 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)

The Unified Event Model

All three transports share the same shape:

HTTP:   fn conn         -> response
WS/SSE: fn event, socket -> reply

Return value dispatch (HTTP handlers):

Return valueWhat Francis sends
Binary string200, no content-type set (use text/2 to force text/plain)
Map or list200 JSON (application/json)
Plug.Conn structSent 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 valueWhat 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 :okNothing sent

{:reply, {:binary, bytes}} is the only way to send binary WebSocket frames.


HTTP Routes

elixir
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)
end

unmatched/1 must be declared last — it shadows any routes declared after it.

Accessing data:

  • Path params + query string: conn.params["id"]
  • Request body: 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):

elixir
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.


WebSockets

elixir
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:

elixir
%{
  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 protection

Module 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.


Server-Sent Events (SSE)

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).

elixir
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:

elixir
{: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\n

Options:

  • :keepalive_interval (default: 15_000 ms) — comment line to keep connection alive; nil to disable

WS vs SSE — critical difference:

WS send(socket.transport, msg)SSE send(socket.transport, msg)
Routes through handler?No — sent directly to clientYes — delivered to {:received, msg}
{:received, _} sourceClient text frames over the wireOther processes only

Show full SKILL.md (293 more words)Show less

Plug Composition

Plugs run before route handlers, in declaration order. Auth plugs must come before route macros.

elixir
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)
end

Router forwarding for scoped middleware:

elixir
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)
end

forward/2 and plug/1-2 are from Plug.Router/Plug.Builder — see Plug docs for full options.


Security

elixir
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: true

redirect/2 and redirect/3 accept relative paths only. Absolute URLs raise ArgumentError. Protocol-relative URLs (//evil.com) are converted to /.


Error Handling

elixir
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")
end

The 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.


Configuration

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.

elixir
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: true

Note: the outer key is singular :parser; the inner Plug.Parsers key is plural :parsers.


Static Assets & Digestion

elixir
use Francis, static: [from: "priv/static", at: "/"]
bash
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)
elixir
Francis.Static.static_path("app.css")  # => "/app-a1b2c3d4.css"

Mix Tasks

bash
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.4

Testing

elixir
defmodule 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
end

Always prefix mix commands with unbuffer: unbuffer mix test


Gotchas & Red Flags

SituationCorrect approach
Rendering user input in HTMLhtml(conn, "<p>#{Francis.HTML.escape(input)}</p>")
Rendering untrusted textsafe_html(conn, input) — escapes entire string; do NOT wrap in markup
SSE pushing from handlerSSE is server→client; push via send(socket.transport, msg) from another process
WS broadcasting from another processsend(socket.transport, msg) bypasses handler — goes direct to client
Forgetting :close handlerSilent success — add {:close, _} to clean up subscriptions and ETS entries
Absolute URL in redirectFrancis raises ArgumentError — relative paths only
dev: true not workingOnly valid in config.exs, not in use Francis opts
Auth inside a route handlerMove 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 shapeGenerate the same module name — silently overwrite each other
unmatched/1 not catching routesMust 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

Files

Just SKILL.md in skills/francis-thinking of francis-build/francis.

Open the folder on GitHubat commit 323c88c

Compare with similar skills

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.

Francis Thinking compared with similar skills
SkillStarsUsed inTokensAuto-checkLicenceRepo updated
Francis Thinking this skillfrancis-build/francis107—~3kAutomated safety check: PassMIT
Cb Security HardeningBlkLeg/CircuitBreaker201—~2.1kAutomated safety check: PassMIT
Openenv Agentic Rlburtenshaw/training-agents153—~381Automated safety check: PassApache-2.0
Vercel Functionsvercel/vercel-plugin301—~12kAutomated safety check: NotesCustom licence
Tgf Server Devthkhxm/tgf128—~1.3kAutomated safety check: NotesMIT
Acarshub Socket Namespacesdr-enthusiasts/docker-acarshub117—~710Automated safety check: PassGPL-3.0

Similar skills

  • Cb Security Hardening

    BlkLeg/CircuitBreaker

    Enforces Circuit Breaker security hardening conventions across backend, frontend, Docker, and nginx.

    201 GitHub stars~2.1k tokensUpdated 5 days ago
    Backend & APIsAuto-check passed
  • Openenv Agentic Rl

    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…

    153 GitHub stars~381 tokensUpdated 28 days ago
    Backend & APIsAuto-check passed
  • Vercel Functions

    vercel/vercel-plugin

    Official

    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…

    301 GitHub stars~12k tokensUpdated yesterday
    Backend & APIsAuto-check: notes
  • Tgf Server Dev

    thkhxm/tgf

    基于 tgf v2(github.com/thkhxm/tgf/v2)用确定性的 tgfctl 工作流创建、验证和维护 Go 游戏服务器项目。

    128 GitHub stars~1.3k tokensUpdated 2 mo ago
    DatabasesAuto-check: notes
  • Acarshub Socket Namespace

    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.

    117 GitHub stars~710 tokensUpdated 3 days ago
    DevOps & CloudAuto-check passed
  • 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

    13k GitHub stars~4k tokensUpdated yesterday
    Backend & APIsAuto-check: notes

Works with

Questions about Francis Thinking

What does Francis Thinking do?

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.

When should I use Francis Thinking?

Francis Thinking fits situations like: tasks that involve Realtime and WebSockets; tasks that involve Containers.

How do I install Francis Thinking in Claude Code?

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.

How do I install Francis Thinking in Codex?

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.

Can I use Francis Thinking in Cursor, Gemini CLI or GitHub Copilot?

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.

What does Francis Thinking need to run?

SKILL.md names no scripts, command-line tools or credentials: Francis Thinking is instructions for the agent only.

Does Francis Thinking access the network?

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.

Is Francis Thinking safe to install?

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.

What licence does Francis Thinking use?

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.

How many tokens does Francis Thinking use?

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.

What are the alternatives to Francis Thinking?

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.

Who maintains Francis Thinking?

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.