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

```html
<?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 `iprint`s. `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`.

- **Form fields and query parameters** both arrive through
  `icpp_getparam("name")`, URL-decoded, "" when absent. A POST body in
  `application/x-www-form-urlencoded` is merged into the same place, so a
  field is read the same way whether the form was GET or POST.
- **Was it a POST?** `icpp_strcmp(icpp_getenv("REQUEST_METHOD"), "POST") == 0`.
- **Cap every field**: `icpp_substr(icpp_trim(icpp_getparam("title")), 0, 120)`.
- **Headers** (Set-Cookie, Status, Location, Content-Type) cannot be printed:
  the server has already sent its own. They are appended, one per line, to
  the file named in the environment variable `ICPP_HEADERS_FILE`
  (`NOUHATA_HEADERS_FILE` on an Apache host set up like nouhata.com; set both
  to the same file when you write a server). The server emits that file
  before the body. Helper:

```c
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);
}
```

- **After a POST that changed something, redirect** (303) to a GET page. Do
  the work, `icpp_sql_disconnect(db)`, then `app_redirect(...)`. Reloading
  never repeats the change.
- **Cookies**: read `icpp_getenv("HTTP_COOKIE")` and find your name in it
  (`icpp_re(icpp_concat("; ", cookie), "; sid=[0-9a-f]+")`); set one with
  `Set-Cookie: sid=...; Path=/; HttpOnly; SameSite=Lax; Max-Age=2592000`
  through `app_header`. A session is a row in a `sessions` table keyed by a
  random id from `icpp_random_hex(16)`.
- **CSRF**: keep a 32-hex token in the session row, print it as a hidden
  `csrf` field in every form, and refuse a POST whose `csrp` field differs.
- **File uploads** (multipart) need the server's help; the Windows web suite
  and the Apache handler support them, the Runner does not yet.

---

## 4. The database (InternetSQL through `icpp_sql_*`)

```c
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):

```c
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:

```html
<?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

| Need | Builtin |
|---|---|
| Print into the page | `iprint(a, b, ...)` (newline appended) |
| Build a string | `icpp_concat(a, b, ...)` |
| Compare, length, find | `icpp_strcmp(a, b)` (0 = equal), `icpp_strlen(s)`, `icpp_strstr(hay, needle)` (-1 = absent) |
| Slice, trim, case, replace | `icpp_substr(s, start, len)`, `icpp_trim(s)`, `icpp_upper(s)`, `icpp_lower(s)`, `icpp_replace_all(s, from, to)` |
| Regex match / first substitution | `icpp_re(s, "pattern")`, `icpp_re(s, "s/pat/rep/")` |
| Split / lists | `icpp_split(s, sep)` -> list; `icpp_list_new/add/get/count/free` |
| Maps, sets | `icpp_map_new/set/get/has/free`, `icpp_set_new/add/has` |
| Numbers | `icpp_atoi(s)`; `icpp_concat("", n)` |
| Escape for HTML | `icpp_html_escape(s)` |
| Request | `icpp_getparam(name)`, `icpp_getenv(name)` |
| Database | `icpp_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)` |
| Files | `icpp_file_read(path)` (about 4 KB), `icpp_file_write(path, text)`, `icpp_file_exists(path)`, `icpp_glob(pattern)` |
| Time | `icpp_time()` (epoch seconds), `icpp_date_string()` ("YYYY-MM-DD HH:MM:SS", local) |
| Random, hashing | `icpp_random_hex(nbytes)`, `icpp_sha256(s)`, `icpp_hmac_sha256(key, s)` |
| HTTPS | `icpp_https_get(url, headers)`, `icpp_https_post(url, headers, body)` |
| JSON | `icpp_json_get(json, "a.b.0.c")` |
| QR and barcodes | `icpp_qrcode_svg(text, px)`, `icpp_barcode_svg(text, "code128" or "ean13", px)`, `icpp_qrcode_read(path)`, `icpp_barcode_read(path)` |
| Stop | `icpp_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:

```sh
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:

- **A fresh database per suite.** Suites that share one database fail for
  reasons that look like bugs (a test changed a password, suspended a user).
- **Grep uses basic regex**: a `*` or `.` in the expected text needs `\*`, `\.`.
- **A page that dies mid-render can still answer HTTP 200** with a short
  body. Check content, not only status.
- **Compile errors are printed by kcpp** and name the generated file and a
  line; the Runner shows them in the browser, an Apache host logs them.
- **Same-second timestamps** sort in arbitrary order; find a row by a key you
  chose, not by "the first one on the page".

---

## 8. Where things run

| Host | How a page is run | Headers file variable | Notes |
|---|---|---|---|
| ICPP Web Suite (Windows) | iwebserver -> icpptpl -> kcpp | ICPP_HEADERS_FILE | `iweb`, http://localhost:8098/ |
| ICPP Runner (Android) | Java server -> icpptpl -> kcpp | ICPP_HEADERS_FILE and NOUHATA_HEADERS_FILE | site zip installed from the Sites page; `env.txt` read; db/*.sql run once |
| Apache on Linux | Action handler (nouhata-cgi) -> icpptpl -> kcpp | NOUHATA_HEADERS_FILE | one process per request; MaxRequestWorkers x ~40 MB per render |
| Command line | `kcpp -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.
