# Lua Vibe

> Lua best practices, idioms, and ecosystem tooling. Use when writing or refactoring Lua code; designing modules; choosing between locals, tables, and metatables; handling errors with pcall/xpcall/assert; building OOP via __index; tuning performance for LuaJIT; setting up LuaRocks, busted tests, lua-language-server, stylua, luacheck, or selene; or writing LuaCATS type annotations.

- **Type:** Skill
- **Install:** `agentstack add skill-bpavlo-agent-skills-lua-vibe`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [bpavlo](https://agentstack.voostack.com/s/bpavlo)
- **Installs:** 0
- **Category:** [Agent Skills](https://agentstack.voostack.com/c/agent-skills)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [bpavlo](https://github.com/bpavlo)
- **Source:** https://github.com/bpavlo/agent-skills/tree/main/lua-vibe

## Install

```sh
agentstack add skill-bpavlo-agent-skills-lua-vibe
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## About

# lua-vibe — Lua Best Practices and Ecosystem

## When to Use This Skill

Load this skill whenever the task involves:

- Writing new Lua code or refactoring existing modules.
- Choosing between `local` and a (rarely correct) global, deciding when to use `pcall`, designing a metatable-based "class," or picking between `..` / `string.format` / `table.concat`.
- Resolving `nil`-handling, multiple-return, or truthiness pitfalls.
- Setting up a new project: `rockspec`, `package.path`, directory layout.
- Running tests with `busted` (or compatible runners).
- Configuring `lua-language-server` (`lua_ls`), `stylua`, `luacheck`, or `selene`.
- Adding LuaCATS (`---@`) annotations so editors can type-check call sites.
- Diagnosing performance: hot loops, allocation in tight code, JIT abort patterns, weak tables for caches.
- Reviewing someone's Lua for the standard set of anti-patterns (globals, over-`pcall`, string concatenation in loops, ipairs vs numeric for).

The skill targets **Lua 5.1 / LuaJIT** as the lowest common denominator — what most embedders ship. Differences in 5.2/5.3/5.4 are flagged inline.

## Source Material

- *Programming in Lua* (Roberto Ierusalimschy) — canonical book, multiple editions.
- *Lua 5.1 Reference Manual* — `https://www.lua.org/manual/5.1/`. Authoritative for the language LuaJIT runs.
- *lua-users wiki* — `http://lua-users.org/wiki/` — large body of idiomatic snippets.
- *LuaJIT documentation* — `https://luajit.org/` — JIT semantics, NYI list, performance guide.
- *LuaCATS specification* — `https://luals.github.io/wiki/annotations/` — annotation grammar consumed by `lua_ls`.
- *busted documentation* — `https://lunarmodules.github.io/busted/`.
- *LuaRocks documentation* — `https://github.com/luarocks/luarocks/wiki/`.

## Core Philosophy

These principles are evaluated in order. Each later principle assumes the earlier ones.

### 1. `local` everywhere

Every name that doesn't need to escape its file is `local`. Modules expose their public surface via a single returned table. Globals exist for two reasons only: legacy interop with the embedder, and deliberate convenience symbols (which should be documented and namespaced).

### 2. Tables are the only collection

Lua has one container. Pick a *shape* per variable — array, record, or set — and don't switch shapes mid-pipeline. A function should not return an array sometimes and a record other times.

### 3. Errors are values; `pcall` is for *recoverable* failure

`pcall` is for expected failures (optional dependency, network call, parsing user input). Wrapping every operation in `pcall` produces silent half-failures. Use `assert` for programmer errors and `error(msg, level)` to attribute to the caller.

### 4. Metatables are a tool, not a paradigm

`__index`, `__newindex`, `__call`, and `__tostring` cover 95% of useful metatable work. Inheritance via `__index` is fine; deeper-than-two-level hierarchies are usually wrong. Composition is almost always clearer.

### 5. String building scales with O(n), not O(n²)

`..` is fine for two or three operands. Beyond that, `string.format` for clarity, `table.concat` for loops. Repeated `..` in a loop is O(n²) on the GC.

### 6. Hot paths localize, cold paths read

In a hot inner loop, cache hash lookups (`local fmt = string.format`) and prefer numeric `for i = 1, #t do` over `ipairs`. Outside hot paths, write whichever is clearer. Profile before optimizing — most "slow Lua" is one bad table allocation, not idiomatic style.

### 7. Annotate the public surface, not the private

Add `---@param`, `---@return`, `---@class`, and `---@type` to anything in `M.*`. Internal helpers can stay untyped. Annotations are the contract; types unconsumed by tooling are noise.

### 8. Tooling pays for itself

Run `stylua` (or `lua-format`) on save, `luacheck` or `selene` in CI, `lua-language-server` in your editor. Each catches a different class of bug. The combined runtime cost is sub-second; the bug-prevention rate is high.

### 9. Pin LuaRocks dependencies

A rockspec without a version constraint is a time bomb. Pin to a specific tag or commit; bump deliberately. The same applies to vendored copies in `vendor/` or git submodules.

### 10. Lua 5.1 unless you've checked

Every embedded host has its own Lua version. Default to writing Lua 5.1-compatible code (LuaJIT also runs this) unless you've verified the host supports later. The 5.1 → 5.2/5.3 differences (integer division, bitwise ops, `goto`, `_ENV`) are easy to invoke accidentally.

---

## Language

### Naming

- `snake_case` for variables and functions.
- `PascalCase` for tables intended to be instantiated via metatables (i.e. "classes").
- `SCREAMING_SNAKE_CASE` for module-level constants.
- `_name` underscore prefix for module-private values that are exposed only because Lua has no real privacy.
- `M` (single letter) for the module's returned table at the top of a file. This convention is universal.
- `self` for the implicit first argument of methods declared with `:`.

```lua
local M = {}
local PRIVATE_DEFAULT = 64

local function _internal_helper() end
function M.public_thing() end

return M
```

### Variables and Scope

`local` is mandatory in practice. The omission produces a global, which:

- Costs a hash lookup on every access.
- Pollutes a shared namespace where other code can clobber it.
- Defeats `lua_ls` "undefined global" diagnostics.

```lua
-- Wrong: implicit globals
function helper(x) return x * 2 end       -- creates _G.helper
my_state = {}                              -- creates _G.my_state

-- Right
local function helper(x) return x * 2 end
local my_state = {}
```

In hot paths, also localize stdlib functions:

```lua
local fmt   = string.format
local concat = table.concat
local insert = table.insert
```

The optimization is real (LuaJIT registers vs hash table), but the larger benefit is diagnostic clarity.

### Truthiness

Only `false` and `nil` are falsy. Everything else is truthy, including `0`, `""`, and `{}`.

```lua
if x then ... end           -- x is not nil and not false
if x == nil then ... end    -- explicit nil
if not x then ... end       -- x is nil or false (sometimes ambiguous)
```

When `nil` and `false` mean different things, write the explicit comparison.

### `and` / `or` for Defaults and Ternaries

Lua has no ternary. The idiom is `cond and a or b`, valid as long as `a` is never falsy:

```lua
local label = ok and "yes" or "no"
```

If `a` can legitimately be `false` or `nil`, switch to `if`:

```lua
local val
if ok then val = first_thing else val = fallback end
```

For default arguments:

```lua
function M.greet(name)
  name = name or "world"          -- works as long as name == false isn't valid
  ...
end

function M.toggle(verbose)
  if verbose == nil then verbose = true end   -- distinguishes nil from false
  ...
end
```

### Tables

Lua has one collection. The same table can be:

- An **array** (numeric keys 1..n, no holes): `{1, 2, 3}`. Iterate with `ipairs` or `for i = 1, #t do`.
- A **record** (string keys): `{ name = "ada", year = 1815 }`. Iterate with `pairs`.
- A **set** (keys are members, value is `true`): `{ x = true, y = true }`. Test with `s[k]`.
- A **hybrid**: `{ "first", "second", name = "list" }`. Allowed but read carefully.

Pick a shape per variable. A function that returns an array sometimes and a record other times has one too many responsibilities.

```lua
-- Array
for i, v in ipairs(items) do ... end
for i = 1, #items do local v = items[i]; ... end   -- faster

-- Record
for k, v in pairs(record) do ... end

-- Set
local seen = {}
for _, x in ipairs(items) do
  if not seen[x] then
    seen[x] = true
    process(x)
  end
end
```

#### Length and Holes

`#t` returns *some* boundary in `t`. It is well-defined only for sequences (no `nil` holes). If your array can be sparse, store `n` explicitly:

```lua
local t = { n = 0 }
local function push(v) t.n = t.n + 1; t[t.n] = v end
```

`table.pack(...)` (5.2+, polyfilled in 5.1) and `select("#", ...)` give vararg counts.

#### Iteration Order

`pairs(t)` iteration order is **unspecified** for hash entries. If you need stable output, collect keys into an array and `table.sort` them.

```lua
local keys = {}
for k in pairs(t) do keys[#keys + 1] = k end
table.sort(keys)
for _, k in ipairs(keys) do print(k, t[k]) end
```

### Multiple Return Values

Lua functions return any number of values. Two pitfalls:

- Trailing returns are discarded when the call is not in the last position:
  ```lua
  local a, b = f(), g()      -- only first value of f(); both values of g()
  print(f(), g())            -- only first value of f(); both values of g()
  ```
- Wrapping a call in parentheses truncates to one:
  ```lua
  local a, b = (f())         -- b is always nil
  ```

Use this deliberately when discarding:

```lua
local n = select("#", ...)              -- count varargs
local _, _, third = string.find(...)    -- skip first two captures
```

### Strings

#### Building

`..` allocates and copies both operands. Fine for two or three:

```lua
local greeting = "hello, " .. name .. "!"
```

In loops, use `table.concat`:

```lua
local parts = {}
for i, line in ipairs(lines) do
  parts[i] = line:upper()
end
local joined = table.concat(parts, "\n")
```

For format-style output, use `string.format`:

```lua
local msg = string.format("[%s] %d errors in %s", level, n, file)
```

`%q` quotes a string for round-tripping through `load`/`loadstring`. `%s` calls `tostring` on its argument.

#### Patterns

Lua patterns are *not* regex. They're simpler and faster, with a different syntax. The metacharacters:

```
.   any character
%a  letter           %A  not letter
%d  digit            %D  not digit
%s  whitespace       %S  not whitespace
%w  alphanumeric     %W  not alphanumeric
%p  punctuation      %P  not punctuation
%l  lowercase        %L  not lowercase
%u  uppercase        %U  not uppercase
%c  control          %C  not control
%x  hex              %X  not hex

[]  character class      [^]  negated class
?   0 or 1               *    0 or more (greedy)
+   1 or more (greedy)   -    0 or more (non-greedy)
^   start of subject     $    end of subject
%   escapes magic chars: ( ) . % + - * ? [ ] ^ $
()  capture group
```

```lua
-- Find
local i, j = string.find(s, "%d+")           -- positions of first digit run

-- Match (returns captures or full match)
local year = s:match("(%d%d%d%d)")

-- Iterate matches
for word in s:gmatch("%S+") do print(word) end

-- Replace
local out = s:gsub("%s+", " ")               -- collapse whitespace
```

For real regex, use a library (`lpeg`, `lrexlib`).

#### Common Idioms

```lua
-- Trim
local trimmed = s:match("^%s*(.-)%s*$")

-- Split on a single char
local function split(s, sep)
  local out = {}
  for part in s:gmatch("([^" .. sep .. "]+)") do out[#out + 1] = part end
  return out
end

-- Starts with / ends with
local function starts_with(s, prefix) return s:sub(1, #prefix) == prefix end
local function ends_with(s, suffix)   return suffix == "" or s:sub(-#suffix) == suffix end
```

5.3+ adds `string.pack` / `string.unpack` for binary serialization.

### Control Flow

```lua
if a then ... elseif b then ... else ... end

while cond do ... end
repeat ... until cond                 -- post-test; cond is true to STOP
for i = 1, 10 do ... end
for i = 10, 1, -1 do ... end
for k, v in pairs(t) do ... end

-- 5.2+: break and goto
for i = 1, n do
  if skip(i) then goto continue end
  ...
  ::continue::
end
```

Lua 5.1 has `break` but no `continue`. Use `goto continue` or refactor.

### Functions

Lua functions are first-class values. Common forms:

```lua
local function f(x) return x + 1 end       -- declarative, recommended
local f = function(x) return x + 1 end     -- expression form

-- Method declaration: : injects implicit self
function Animal:speak() print(self.name) end
-- equivalent to:
Animal.speak = function(self) print(self.name) end

-- Method call: : passes receiver as first arg
animal:speak()
-- equivalent to:
animal.speak(animal)
```

`local function` is sugar for `local f; f = function() ... end`. Order matters for self-reference:

```lua
-- Doesn't work: a's body captures b before b exists
local function a() return b() end
local function b() return 1 end

-- Works: declare both, then assign
local a, b
function a() return b() end
function b() return 1 end
```

Varargs are accessed as `...` and counted with `select("#", ...)`:

```lua
local function variadic(...)
  local n = select("#", ...)
  for i = 1, n do
    local arg = select(i, ...)
    print(i, arg)
  end
end
```

### Closures

Inner functions capture outer locals by reference (upvalues). Useful for state hiding:

```lua
local function counter()
  local n = 0
  return function() n = n + 1; return n end
end

local next_id = counter()
print(next_id(), next_id(), next_id())   -- 1, 2, 3
```

Each call to `counter()` produces an independent closure with its own `n`.

---

## Error Handling

### `error` and `assert`

```lua
error("bad input")              -- raises with message; level 1 (this function)
error("bad input", 2)           -- attribute error to caller (preferred for libraries)
error({ code = "EBAD" })        -- non-string errors are allowed

assert(cond, "must be true")    -- if cond is falsy, raises "must be true"
local h = assert(io.open(path)) -- idiom: raise on nil return
```

`error(msg, level)` controls which stack frame the error attributes to. `level = 0` suppresses the position. `level = 2` blames the caller — use it in library code.

`assert(v, msg)` is shorthand for `if not v then error(msg or "assertion failed", 2) end`. Use it for *preconditions* and to unwrap nilable returns.

### `pcall` / `xpcall`

`pcall` runs a function in protected mode. Returns `(true, results...)` on success, `(false, err)` on failure.

```lua
local ok, result = pcall(do_thing, arg)
if not ok then
  -- result is the error message (or value passed to error())
  log_error(result)
  return
end
-- use result
```

`xpcall` is `pcall` plus a handler that runs *inside* the failing frame, so `debug.traceback` produces a useful trace:

```lua
local ok, err = xpcall(do_thing, debug.traceback, arg)
if not ok then print(err) end       -- err is "msg\nstack traceback: ..."
```

### When to `pcall`

Use `pcall` only where:

1. Failure is *expected* (parsing user input, optional dependency, network).
2. You can *recover meaningfully* (fall back to a default, skip a row, retry).

Don't use `pcall` to "be safe." Wrapping every call hides bugs and produces silent half-failures. Prefer letting errors propagate; the embedder usually has a top-level error handler.

```lua
-- Good: optional dependency
local ok, lib = pcall(require, "optional-thing")
if ok then lib.setup() end

-- Bad: shotgun pcall
local ok1, x = pcall(get_x)
if not ok1 then return end
local ok2, y = pcall(get_y)
if not ok2 then return end
local ok3 = pcall(do_thing, x, y)        -- silently fails, no stack trace
```

### Error Objects

`error(value)` accepts any value, not just strings. Returning a structured error from a library is common:

```lua
local function load_config(path)
  local f, err = io.open(path)
  if not f then
    error({ code = "EOPEN", path = path, cause = err }, 2)
  end
  ...
end

local ok, err = pcall(load_config, "/missing")
if not ok and type(err) == "table" and err.code == "EOPEN" then
  -- handle structured
end
```

For interop, also stringify on the way out so `tostring(err)` is human-readable:

```lua
setmetatable(err, { __tostring = function(self) return self.code .. ": " .. self.path end })
```

### `pcall` and `xpcall` Differences

| Function | Handler runs in failing frame? | Use when |
|---|---|---|
| `pcall(fn, ...)` | No | You need only the message |
| `xpcall(fn, handler, ...)` | Yes | You want a stack trace; you want a custom transformation of the error |

In Lua 5.1, `xpcall` doesn't accept arguments after the handler:

```lua
-- 5.1: pass via closure
xpcall(function() return do_thing(arg) end

…

## Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [bpavlo](https://github.com/bpavlo)
- **Source:** [bpavlo/agent-skills](https://github.com/bpavlo/agent-skills)
- **License:** MIT

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** no
- **Filesystem access:** yes
- **Shell / process execution:** no
- **Environment & secrets:** yes
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/skill-bpavlo-agent-skills-lua-vibe
- Seller: https://agentstack.voostack.com/s/bpavlo
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
