Debug a Failing Skill
When a Lua tool throws or returns an unexpected result, CUST/OS gives you several ways to figure out what went wrong.
The fast path: in-app debug
When a tool fails during chat, the chat panel shows the error inline with a [Debug] link:
Tool failed: place_marker -- attempt to call a nil value (method 'getPoint')
[Debug]
Tap [Debug]. The editor opens with:
- The failing
.luafile loaded - The cursor on the offending line
- The error message in the bottom debug bar
- The Lua stack trace expanded under the bar
Edit the file, save, and re-issue the original chat message. The hot-reload picks up your fix immediately. No restart required.
Per-message debug panel
Swipe left on any agent message bubble to reveal Debug and Delete buttons.
Tap Debug to see the full assembled context for that turn:
- Selected skills with relevance scores
- Full system prompt
- Tool definitions
- Conversation window (what the LLM actually saw)
- Estimated token count
- Provider and tier
Use the Copy button to grab the full context for sharing or analysis.
Logcat (for non-visible errors)
For initialization failures, sandbox violations, or file watcher errors:
adb logcat | grep "custos"
Common messages and what they mean:
| Message | Meaning |
|---|---|
Loaded skill custos.X with N tools |
Skill parsed and registered successfully |
Failed to parse SKILL.md ... |
YAML frontmatter is malformed |
Failed to parse @tool annotation in ... |
LDoc comment block is missing or wrong |
Lua execution error: ... |
Runtime error inside the script |
Access denied: <class> |
Script tried to import a class not on the allowlist |
Method blocked: <method> |
Script tried to call a blocked method |
Lua instruction limit exceeded |
Script hit the instruction limit |
attempt to call a nil value (method ...) |
Wrong method name or wrong receiver type |
Security audit entries
Open the Audit panel and filter to SECURITY. Sandbox violations show up here with the offending class or method. Check this first when an existing skill suddenly stops working -- it usually means the allowPackages list in custos.yaml was tightened.
Common failure modes
attempt to call a nil value on a Java object
Almost always means you are using dot syntax instead of colon syntax. Colon passes the receiver as self, which the Lua/Java bridge requires:
-- WRONG
local point = item.getPoint()
-- RIGHT
local point = item:getPoint()
forEach callback fires once and stops
The Lua/Java bridge passes self as the first argument to interface callbacks, which breaks iteration. Use an iterator instead:
-- WRONG
rootGroup:deepForEachItem(function(item) ... return true end)
-- RIGHT
local items = rootGroup:getItemsRecursive()
local iter = items:iterator()
while iter:hasNext() do
local item = iter:next()
-- ...
end
If you must use a callback-based interface (e.g., tile capture), declare self as the first parameter:
tileCapture:capture(params, {
onCaptureTile = function(self, tile, tileNum, col, row)
-- self is the proxy; tile is the real first arg
end,
})
null where you expect a Java array
Lua tables do not auto-convert to Java arrays. Build the array explicitly:
local Array = import("java.lang.reflect.Array")
local points = Array:newInstance(GeoPoint, 4)
Array:set(points, 0, p1)
Array:set(points, 1, p2)
Array:set(points, 2, p3)
Array:set(points, 3, p4)
someMethod(points) -- now works
length or [] does not work on Java arrays
Java arrays from reflection do not expose length or index syntax. Use explicit accessors:
local Array = import("java.lang.reflect.Array")
local methods = clazz:getMethods()
local n = Array:getLength(methods)
for i = 0, n - 1 do
local m = Array:get(methods, i)
-- ...
end
Method renamed by obfuscation
If you are calling a method on a third-party plugin object and you get attempt to call a nil value, the method was likely renamed by obfuscation. Do not guess renamed methods -- use broadcast intents instead:
local Intent = import("android.content.Intent")
local AtakBroadcast = import("com.atakmap.android.ipc.AtakBroadcast")
local intent = Intent("com.atakmap.android.grgbuilder.SHOW_BUILDER")
intent:putExtra("uid", params.uid)
AtakBroadcast:getInstance():sendBroadcast(intent)
Use discover_api(query="intents:<keyword>") to find the right action string.
Math function missing
The Lua math library is missing some functions like atan2. Use java.lang.Math instead:
local Math = import("java.lang.Math")
local angle = Math:atan2(dy, dx)
Marker placed but not findable
addItem() directly on the root group renders the marker but it does not go through the proper CoT pipeline, so other tools cannot find it. Use the CoT pipeline instead -- see the marker placement pattern in How-to: Write a skill.
Iterating quickly
There is no offline Lua test harness. Iterate fast by:
- Open the editor
- Save your change
- Switch to the chat panel
- Type a one-line prompt that exercises the tool
- Watch the result -- fix and repeat
Hot-reload makes this loop about 5 seconds per iteration.
Inspecting tool inputs and outputs
Add logging to your script:
local Log = import("android.util.Log")
function my_tool(params)
Log.d("MyTool", "params: " .. tostring(params.callsign))
-- ...
end
Then check adb logcat | grep MyTool.
When the agent picks the wrong skill
If your skill never gets selected:
- Check the description and tags in
SKILL.md-- are they specific enough? - Add representative
examples:queries -- these dramatically improve selection accuracy. - Add synonyms to the tags.
- Verify the embedding provider is online -- without embeddings, only keyword matching is used.