Developer tools · ICPP User Manual · This page as Markdown (save it as CLAUDE.md in your project)

ICPP for AI agents

Rules for writing ICPP web sites with an AI coding agent (Claude Code, Codex, Cursor, or any model that reads a file of instructions). Copy this file into the project as CLAUDE.md, AGENTS.md or .cursorrules, or paste it at the top of the first prompt. Every rule here was measured against the shipped interpreter (build 1.0.0 of 2026-09-30), not guessed. The full reference is the ICPP User Manual beside this file; this page is what the manual does not say in the first ten minutes.

Written for people who build with the ICPP Web Suite (Windows), the Linux kcpp drop, the ICPP Runner on Android, or an Apache host. The nouhata.com marketplace (about 40 pages, 700 automated checks) is written exactly this way, and every pattern below comes from it.


1. What you are writing

An ICPP site is a folder:

public/          the pages; index.icpp.html is the front page; css, js, images beside them
lib/             ICPP code every page #includes (helpers); never served
db/              SQL files that create the database and its tables, run once, in name order
env.txt          KEY=VALUE lines the pages read with icpp_getenv() (Runner and iwebserver)

A page is HTML with <?icpp ... ?> blocks. All the blocks of one page are folded into ONE main() and run by kcpp -icpp for every request, in a fresh process. Output written with iprint(...) lands where the block is. There is no session in memory between requests: everything persists in the database or in cookies.

<?icpp
#include "../lib/app.c"
int db; int n;
db = app_db();
n = 0;
?>
<!doctype html>
<html><head><meta charset="utf-8"><title>Things</title></head><body>
<h1>Things</h1>
<?icpp
$r = icpp_sql_query(db, "select thing_id, name from things order by thing_id;");
while (icpp_sql_next($r))
{
    n = n + 1;
    iprint("<p>", icpp_html_escape(icpp_trim(icpp_sql_get($r, "name"))), "</p>");
}
icpp_sql_free($r);
if (n == 0) iprint("<p>Nothing yet.</p>");
icpp_sql_disconnect(db);
?>
</body></html>

#include "../lib/app.c" inside the first block is hoisted above main(), so a library file holds function definitions and nothing else runs at include time. A page cannot define its own functions: helpers go in lib/.


2. The language rules that break generated code

These are the mistakes every model makes on its first ICPP page. Each was measured.

  1. Definition order is call order. A function must be defined above any call that passes it arguments. There are no prototypes and no headers. In lib/, put the lowest-level helpers first.
  2. Declare with C types, or use $name. int db; char *s; are ordinary locals. $name is an untyped local that needs no declaration and takes whatever it is assigned (number, string, handle). $name may only appear inside a function; in a page every block is inside main(), so it is fine there. In a standalone .icpp script, write a main() and put the code in it.
  3. Handles are int. A database connection, a query result, a list, a map and a set are all int handles. A function that takes one declares int, not a pointer. Free what you open: icpp_sql_free, icpp_list_free, icpp_map_free, icpp_sql_disconnect.
  4. Never index or dereference a char *. s[0], *s, pointer arithmetic and s == "x" are wrong (the last compares addresses). Every string operation is a builtin: icpp_strcmp, icpp_strlen, icpp_substr, icpp_strstr, icpp_trim, icpp_replace_all, icpp_split, icpp_re.
  5. Build strings with icpp_concat(...), any number of arguments, numbers welcome: icpp_concat("id=", id, "&n=", n). Do not join with + (measured: $t + "cd" did not join). Numbers to text: icpp_concat("", n). Text to number: icpp_atoi(s) (stops at the first non-digit, "" gives 0).
  6. iprint appends a newline after each call. Print whole tags in one call; never split a tag across two iprints. iprint takes any number of arguments and no format string; printf exists for C-style formats.
  7. Regex escapes need a double backslash. Inside an ICPP string, \. does not reach the regex engine; write \\. or use a class: [.], [?]. icpp_re(text, "^[0-9]+$") returns the matched text or "". To test "is the whole string digits", compare icpp_strlen(icpp_re(s, "^[0-9]+$")) with icpp_strlen(s).
  8. Escape everything from a visitor before printing it: icpp_html_escape(s) for HTML, and see the SQL rule below. Every iprint of a database value or a form field goes through it.
  9. continue, break, for, else if, ?:, += all work. Loop with while or for. Recursion works.
  10. A $var keeps its last type. Do not assign a number and later a string to the same $var; use two names.

3. Requests, forms, cookies, redirects

The page runs as a CGI program. The web server (iwebserver, the Runner, Apache with nouhata-cgi) sets the usual variables; read them with icpp_getenv: REQUEST_METHOD, QUERY_STRING, HTTP_COOKIE, HTTP_USER_AGENT, REMOTE_ADDR, REQUEST_URI, HTTPS.

void app_header(char *line)
{
    $path = icpp_getenv("ICPP_HEADERS_FILE");
    if (icpp_strlen($path) == 0) return;
    $cur = icpp_file_read($path);
    icpp_file_write($path, icpp_concat($cur, line, "\r\n"));
}

void app_redirect(char *url)
{
    app_header("Status: 303 See Other");
    app_header(icpp_concat("Location: ", url));
    iprint("<a href=\"", icpp_html_escape(url), "\">Continue</a>");
    icpp_exit(0);
}

4. The database (InternetSQL through icpp_sql_*)

int db; int r;
db = icpp_sql_connect(icpp_getenv("ICPP_DB_HOME"), "shop");   // home directory, database name
r = icpp_sql_query(db, "select thing_id, name from things where thing_id=5;");
if (icpp_sql_next(r)) iprint(icpp_trim(icpp_sql_get(r, "name")));
icpp_sql_free(r);
icpp_sql_disconnect(db);

Rules, all measured on InternetSQL 1.25:

  1. Every value in SQL is either an int or a quoted string with ' doubled. There are no prepared statements. One helper does all the quoting: char *q(char *v) { return icpp_concat("'", icpp_replace_all(v, "'", "''"), "'"); } and every string reaches SQL through it. Numbers are icpp_atoi'd first.
  2. Name the columns in every INSERT: insert into things (thing_id, name, made_utc) values(...). A positional INSERT breaks silently the day a column is added.
  3. ORDER BY only on columns in the SELECT list. Ordering by a column you did not select returns no rows and no error.
  4. One JOIN per SELECT. Denormalise instead: keep a copy of the name you display (shop_name on the product row) rather than joining twice.
  5. No count(*). Count rows in a loop, or keep counters.
  6. Trim what you read: char columns come back padded in some cases, so icpp_trim(icpp_sql_get(r, "name")) everywhere.
  7. Ids are yours to allocate. No autoincrement: keep a seq_counters table (name, next_value), read, add one, write back, and take a lock around it (a lock is a file you create with icpp_file_write and remove; two requests are two processes).
  8. Timestamps are text "YYYY-MM-DDTHH:MM:SSZ" in char(24); they sort and compare as strings. Money is whole cents in number(19); never floats.
  9. Column limits: char(n) up to 240 on a wide table (the engine refuses more with "Can't add field"); use text(8000) for long text.
  10. A failed statement does not return 0. Check icpp_sql_status(db) when a write may fail, and test writes with a following SELECT in your checks.
  11. The home must exist with a db folder inside before the first connect; create database shop; then creates the database through a connection to "default". Migrations are files db/001.sql, db/002.sql, ... each ending with a row in a schema_versions table, so a deployment applies only the new ones.
  12. Comments only between statements, alone on their line (-- ...); a comment after a column or after ; breaks the next statement.
  13. Every page opens its own connection and closes it. A query on a closed connection returns nothing, silently, so compute redirect targets before icpp_sql_disconnect.

5. Page and library skeleton

lib/app.c, in this order (lowest first):

char *q(char *v)      { return icpp_concat("'", icpp_replace_all(v, "'", "''"), "'"); }
char *h(char *v)      { return icpp_html_escape(v); }
char *field(char *name, int maxlen) { return icpp_substr(icpp_trim(icpp_getparam(name)), 0, maxlen); }
int   is_post(void)   { if (icpp_strcmp(icpp_getenv("REQUEST_METHOD"), "POST") == 0) return 1; return 0; }
void  app_header(char *line) { ... as above ... }
void  app_redirect(char *url) { ... as above ... }
int   app_db(void)    { return icpp_sql_connect(icpp_getenv("ICPP_DB_HOME"), "shop"); }
void  app_exec(int db, char *sql) { $r = icpp_sql_query(db, sql); icpp_sql_free($r); }
char *app_one(int db, char *sql, char *col)
{
    $v = "";
    $r = icpp_sql_query(db, sql);
    if (icpp_sql_next($r)) $v = icpp_trim(icpp_sql_get($r, col));
    icpp_sql_free($r);
    return $v;
}
void  page_head(char *title) { iprint("<!doctype html><html><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width,initial-scale=1\"><title>", h(title), "</title><link rel=\"stylesheet\" href=\"/site.css\"></head><body>"); }
void  page_foot(void)        { iprint("</body></html>"); }

A page that handles a form:

<?icpp
#include "../lib/app.c"
int db;
db = app_db();
if (is_post())
{
    $name = field("name", 60);
    if (icpp_strlen($name) > 0)
        app_exec(db, icpp_concat("insert into things (thing_id, name, made_utc) values(",
                 icpp_atoi(app_one(db, "select thing_id from things order by thing_id desc;", "thing_id")) + 1,
                 ", ", q($name), ", '", icpp_date_string(), "');"));
    icpp_sql_disconnect(db);
    app_redirect("/");
}
page_head("Things");
?>
<form method="post" action="/"><input name="name" maxlength="60" required><button>Add</button></form>
<?icpp
$r = icpp_sql_query(db, "select thing_id, name from things order by thing_id desc;");
while (icpp_sql_next($r)) iprint("<p>", h(icpp_trim(icpp_sql_get($r, "name"))), "</p>");
icpp_sql_free($r);
icpp_sql_disconnect(db);
page_foot();
?>

(The id allocation above is the short form for a demo; a real site uses a counter table under a lock, rule 4.7.)


6. Builtins you will use on every page

NeedBuiltin
Print into the pageiprint(a, b, ...) (newline appended)
Build a stringicpp_concat(a, b, ...)
Compare, length, findicpp_strcmp(a, b) (0 = equal), icpp_strlen(s), icpp_strstr(hay, needle) (-1 = absent)
Slice, trim, case, replaceicpp_substr(s, start, len), icpp_trim(s), icpp_upper(s), icpp_lower(s), icpp_replace_all(s, from, to)
Regex match / first substitutionicpp_re(s, "pattern"), icpp_re(s, "s/pat/rep/")
Split / listsicpp_split(s, sep) -> list; icpp_list_new/add/get/count/free
Maps, setsicpp_map_new/set/get/has/free, icpp_set_new/add/has
Numbersicpp_atoi(s); icpp_concat("", n)
Escape for HTMLicpp_html_escape(s)
Requesticpp_getparam(name), icpp_getenv(name)
Databaseicpp_sql_connect(home, db), icpp_sql_query(db, sql), icpp_sql_next(r), icpp_sql_get(r, col), icpp_sql_free(r), icpp_sql_status(db), icpp_sql_disconnect(db)
Filesicpp_file_read(path) (about 4 KB), icpp_file_write(path, text), icpp_file_exists(path), icpp_glob(pattern)
Timeicpp_time() (epoch seconds), icpp_date_string() ("YYYY-MM-DD HH:MM:SS", local)
Random, hashingicpp_random_hex(nbytes), icpp_sha256(s), icpp_hmac_sha256(key, s)
HTTPSicpp_https_get(url, headers), icpp_https_post(url, headers, body)
JSONicpp_json_get(json, "a.b.0.c")
QR and barcodesicpp_qrcode_svg(text, px), icpp_barcode_svg(text, "code128" or "ean13", px), icpp_qrcode_read(path), icpp_barcode_read(path)
Stopicpp_exit(code)

The complete list (253 builtins with signatures) is section 4 of the ICPP User Manual.


7. Testing a site the way the interpreter is tested

Write a shell script that starts the server, makes requests with curl and greps the bodies. A check is one line:

get() { curl -s -b "$T/$1" -c "$T/$1" -o "$T/body" -w "%{http_code}" "$B$2" > "$T/code"; }
has() { grep -q -- "$2" "$T/body" && echo "ok    $1" || echo "FAIL  $1 (no '$2')"; }
get anon /; has "front page lists the seeded thing" "First thing"

Rules that save hours:


8. Where things run

HostHow a page is runHeaders file variableNotes
ICPP Web Suite (Windows)iwebserver -> icpptpl -> kcppICPP_HEADERS_FILEiweb, http://localhost:8098/
ICPP Runner (Android)Java server -> icpptpl -> kcppICPP_HEADERS_FILE and NOUHATA_HEADERS_FILEsite zip installed from the Sites page; env.txt read; db/*.sql run once
Apache on LinuxAction handler (nouhata-cgi) -> icpptpl -> kcppNOUHATA_HEADERS_FILEone process per request; MaxRequestWorkers x ~40 MB per render
Command linekcpp -icpp page.icpp or icpptpl page.icpp.html(none)QUERY_STRING and REQUEST_METHOD from the environment

The database home is wherever the host says: ICPP_DB_HOME in the Runner, whatever env.txt or the server configuration sets elsewhere. Read it from the environment, never hard-code a path.


9. A first prompt that works

Build an ICPP site in this folder. Read ICPP_FOR_AI_AGENTS.md first and follow every rule in it. Pages go in public/, helpers in lib/app.c, the schema in db/001.sql. Start with a front page that lists rows from one table and a form that adds one, then run it with iweb (or install the zip in ICPP Runner) and fix what kcpp reports before adding anything else.

Keep the first page small, get it to render, and grow from there. The interpreter reports the line of a mistake; the rules above tell you what it usually is.