# T3ex Config Help

> Configure T3 Code itself — add project scripts (the buttons in the scripts menu), bind keyboard shortcuts, and adjust app settings. Use when the user wants a new T3 Code action, script, shortcut, keybinding, project icon, or a change to how T3 Code behaves.

- Skill: `maddada/t3ex-config-help` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add maddada/t3ex-config-help`
- Raw SKILL.md: https://api.skillmd.com/api/skills/maddada/t3ex-config-help/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: maddada (https://skillmd.com/u/maddada)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/maddada/t3ex-config-help

---


# Configure T3 Code

T3 Code reads configuration from three places. Two are hand-editable; one is
owned by the app.

| What | Where | Edit by hand? |
| --- | --- | --- |
| Project scripts, project icon | `t3.json` at the repo root | Yes — commit it |
| Keyboard shortcuts | `~/.t3/userdata/keybindings.json` | Yes |
| App settings, providers, models | `~/.t3/userdata/settings.json` | No — use the UI |

A dev-mode server (`vp run dev`) uses `~/.t3/dev/` instead of `~/.t3/userdata/`.

> The published docs say keybindings live at `~/.t3/keybindings.json`. That is
> wrong — the code joins the filename to the *state* directory, so the real path
> is `~/.t3/userdata/keybindings.json`. Editing the documented path does nothing.

After any edit, check your work:

```bash
python3 validate.py              # ./t3.json and your keybindings.json
python3 validate.py path/to/t3.json
```

This matters more than usual here: **T3 Code silently ignores invalid rules and
invalid files.** A typo means your shortcut quietly never fires, with only a
warning in the server log.

## Project scripts — the "actions" in the scripts menu

Scripts are the buttons in T3 Code's scripts menu. They run in a T3 Code
terminal at the project root, and they are checked into the repo so the whole
team gets them.

```json
{
  "$schema": "https://t3.codes/schema/t3.json",
  "iconPath": "assets/logo.svg",
  "scripts": [
    {
      "name": "Dev server",
      "command": "pnpm dev",
      "icon": "play",
      "previewUrl": "http://localhost:3000",
      "autoOpenPreview": true
    },
    {
      "name": "Setup Worktree",
      "command": "pnpm install && ln -sf $T3CODE_PROJECT_ROOT/.env .env",
      "icon": "configure",
      "runOnWorktreeCreate": true
    }
  ]
}
```

| Field | Required | Notes |
| --- | --- | --- |
| `name` | yes | Shown in the menu |
| `command` | yes | Shell command, run at the project root |
| `icon` | no | One of `play`, `test`, `lint`, `configure`, `build`, `debug`. Defaults to `play` |
| `runOnWorktreeCreate` | no | Runs automatically after a worktree is created |
| `previewUrl` | no | Opens in the preview panel. **Desktop app only** |
| `autoOpenPreview` | no | Opens the preview the moment the script starts. Needs `previewUrl` |

Limits: 50 scripts, `iconPath` at most 512 characters.

### Two environment variables

Scripts get these injected:

- `T3CODE_PROJECT_ROOT` — the project's main directory, always set
- `T3CODE_WORKTREE_PATH` — the worktree path, only when running in a worktree

This distinction is the point. A script's working directory is the *worktree*
when one exists, so `T3CODE_PROJECT_ROOT` is how you reach back to the main
checkout — which is how you symlink a gitignored `.env` into a fresh worktree.

### Gotchas that cost real time

- **Only the first `runOnWorktreeCreate` script runs.** Marking a second one has
  no effect and no warning. Combine them into one setup script with `&&`.
- **`previewUrl` is desktop-only.** In a browser tab it is ignored and you open
  the URL yourself.
- **`autoOpenPreview` without `previewUrl` does nothing.**
- Scripts are shared with everyone who opens the repo. Don't put machine-specific
  paths or secrets in a `command`.

## Keyboard shortcuts

`~/.t3/userdata/keybindings.json` is a JSON array. Your file replaces the
defaults wholesale, so copy the defaults first if you only want to change one
thing.

```json
[
  { "key": "mod+j", "command": "terminal.toggle" },
  { "key": "mod+shift+d", "command": "terminal.splitVertical", "when": "terminalFocus" },
  { "key": "mod+1", "command": "script.browser.run" }
]
```

`key` and `command` are required; `when` is optional. Maximum 256 rules.

**Modifiers:** `mod` (⌘ on macOS, Ctrl elsewhere), `cmd`/`meta`, `ctrl`/`control`,
`shift`, `alt`/`option`.

**Commands:**

```
sidebar.toggle          rightPanel.toggle       commandPalette.toggle
terminal.toggle         terminal.split          terminal.splitVertical
terminal.new            terminal.close          diff.toggle
preview.toggle          preview.refresh         preview.focusUrl
preview.zoomIn          preview.zoomOut         preview.resetZoom
chat.new                chat.newLocal           editor.openFavorite
modelPicker.toggle      modelPicker.jump.1-9
thread.previous         thread.next             thread.jump.1-9
script.{id}.run
```

**`when` contexts:** `terminalFocus`, `terminalOpen`, `previewFocus`,
`previewOpen`, `modelPickerOpen`. Operators: `!`, `&&`, `||`, parentheses.
Unknown context names evaluate to **false**, so a typo disables the binding
rather than erroring.

**Precedence:** rules are evaluated in array order, and for a given key press the
*last* matching rule wins — across all commands, not just within one. If a
shortcut isn't working, look for a later rule that shadows it.

### Binding a key to your own script

`script.{id}.run` needs the script's **id**, which is not in `t3.json` — T3 Code
assigns it when the script is imported, as a lowercase slug of the name
(`"Browser"` → `browser`). Read the real ids from the state database:

```bash
sqlite3 ~/.t3/userdata/state.sqlite \
  "SELECT title, scripts_json FROM projection_projects WHERE scripts_json != '[]';"
```

The id must match `^[a-z0-9][a-z0-9-]*$` and be at most 24 characters. `validate.py`
cross-checks the ids you reference against the ones that actually exist.

## App settings

`~/.t3/userdata/settings.json` holds app behavior and provider configuration:

- `enableAssistantStreaming`, `enableProviderUpdateChecks`
- `automaticGitFetchInterval`, `newWorktreesStartFromOrigin`
- `defaultThreadEnvMode` — `local` or worktree, for new threads
- `addProjectBaseDirectory` — where the "add project" picker starts
- `textGenerationModelSelection` — the model used for commit messages and titles
- `providers` / `providerInstances` — per-driver config for `codex`, `claudeAgent`,
  `cursor`, `grok`, `opencode`, including binary paths and custom models
- `observability`

**Change these in the UI, not the file.** The server owns this file and rewrites
it; a hand edit made while T3 Code is running will be overwritten without
warning. Read it to answer questions, and to confirm a settings change landed.

Note this file configures which *providers* T3 Code can drive. It does not
configure the agents themselves — Codex still reads `~/.codex/config.toml`, and
MCP servers are configured per-provider, not in T3 Code.

## Answering "why isn't my config working?"

Work down this list:

1. Run `validate.py` — it catches most of it.
2. Check the path. `~/.t3/keybindings.json` (documented) is not the real one.
3. For a shortcut, look for a *later* rule with the same key — last match wins.
4. For a `when` clause, confirm every context name is in the list above.
5. For a script, restart or reopen the project so `t3.json` is re-read.
6. Check the server log for parse warnings: `~/.t3/userdata/logs/server.log`.

