DynamoDB and OpenSearch CLI for AI agents

dynotable-cli lets an AI agent, such as Claude Code, Codex or Cursor, read your DynamoDB tables and OpenSearch indices through the DynoTable app on your computer. The agent runs a command and gets JSON back.

You stop pasting query results into the chat, and the agent never gets your AWS keys. The CLI only reads, and it sees only the profiles that you make available to agents.

What you need

  • DynoTable 1.20.0 or later, installed on the same computer.
  • Editor integration switched on in Settings → Editor integration. It is off by default. See Turn on Editor integration.
  • One or more profiles that are available to agents.

Make a profile available to agents

  1. In DynoTable, open Settings and select the profile.
  2. Open the profile's MCP section.
  3. Switch on Available to agents.

Do this for each profile that agents can read. dynotable-cli cannot list or read the other profiles.

A profile that is not available and a profile that does not exist give the same error, so an agent cannot find out which other profiles you have.

The same switch controls the MCP server. When you switch it off, a running agent loses the profile at its next request.

Install dynotable-cli

  • npm: npm install -g dynotable-cli. This also installs dynotable-lsp.
  • GitHub Releases: download the archive for your platform from Releases, extract it, and put dynotable-cli and dynotable-lsp on your PATH. The macOS archive is not notarized yet, so macOS blocks it after a browser download. On macOS, use npm.
  • Windows: use the Releases archive and put the folder that holds dynotable-cli.exe on your PATH. The Claude Code plugin cannot start the command that npm installs on Windows.

Builds are available for macOS on Apple silicon, Linux x64 and Windows x64. The macOS build is tested on every build. The Linux build has run in a container, and the Windows build has not run yet.

When DynoTable is not running, dynotable-cli starts it with no window, and the app keeps running after the command ends. With --no-launch, the command fails with launcher-off instead.

Commands

dynotable-cli profiles                  # the profiles available to agents
dynotable-cli tables --profile dev      # the DynamoDB tables of a profile
dynotable-cli indices --profile dev     # the OpenSearch indices of a profile
dynotable-cli run orders.dtsql          # run the statement in a file

run also reads a statement from stdin (-) or from the text after -e. Both need --lang sql, --lang partiql or --lang opensearch:

dynotable-cli run -e "SELECT * FROM \"orders\" WHERE customerId = 'C1'" --lang partiql
  • Give one statement per input.
  • A file must be a query file. The CLI refuses every other file.
  • --row-cap <n> sets the most rows that run returns. The default is 500, and the maximum is 1000. truncated: true in the result means that more rows exist.
  • Writes do not run. A PartiQL write fails with stage-in-app. Make the change in DynoTable, where staging holds it for review.
  • dynotable-cli --help shows all options.

Pick a profile

A command uses the first profile it finds:

  1. --profile <id or slug>.
  2. The profile key of .dynotable.json in the current directory.
  3. The only profile available to agents, when there is exactly one.
{"profile": "dev"}

dynotable-cli reads only the profile key.

Output and exit codes

Stdout holds one JSON document. On failure, stderr holds one JSON object with a code and a message that tells what to do:

{
  "code": "profile-not-exposed",
  "message": "The profile is not found, or it is not available to agents. Make the profile available to agents in DynoTable."
}
Exit codeMeaning
0Success.
1Failure. Read code on stderr.
2The command line is not valid (usage).
130Interrupted with Ctrl+C. Stdout is empty.
codeWhat to do
profile-not-exposedMake the profile available to agents in DynoTable.
profile-requiredGive --profile. dynotable-cli profiles lists the choices.
stage-in-appThe CLI only reads. Make writes in DynoTable.
reauth-required, signed-outOpen DynoTable and sign in.
launcher-off, integration-offStart DynoTable, or switch on Editor integration.
agent-refusedThe request is not available to agents. Do it in DynoTable.
update-appUpdate DynoTable.
listing-failedThe listing is not complete. Stdout holds the rows it read.

Query files

FileLanguage
*.dtsqlDynoTable SQL, over tables and indices
*.dynotable.sqlDynoTable SQL, over tables and indices
*.partiqlDynamoDB PartiQL
*.opensearchOne OpenSearch query: JSON DSL or Lucene

The file name gives the language, so do not give --lang with a file. An OpenSearch query names its index on its first line:

// dynotable: index=orders
{"query": {"match": {"status": "shipped"}}}

Install the Claude Code plugin

The plugin gives Claude Code a dynotable skill that runs dynotable-cli for you. It also gives diagnostics and hover in .dtsql, .partiql and .opensearch files. It does not attach to .sql files.

  1. Install dynotable-cli.

  2. In Claude Code, add the marketplace and install the plugin:

    /plugin marketplace add dynotable/claude-plugins
    /plugin install dynotable@dynotable

The plugin works while DynoTable is running with Editor integration on. It does not start DynoTable.

When the app is not running, the plugin's language server waits up to 20 seconds for it and then stops. If diagnostics stop after DynoTable was closed for a while, start a new Claude Code session.

Codex, Cursor and other agents that run shell commands use dynotable-cli directly. Only Claude Code has a plugin.

Limit Claude Code to dynotable-cli

Add this rule to your Claude Code settings (~/.claude/settings.json for every project, or .claude/settings.json in one project):

{
  "permissions": {
    "allow": ["Bash(dynotable-cli:*)"],
    "deny": [
      "Bash(dynotable-lsp:*)",
      "Read(~/Library/Application Support/DynoTable/**)",
      "Read(~/.config/DynoTable/**)",
      "Read(~/AppData/Roaming/DynoTable/**)"
    ]
  }
}

The three Read paths are DynoTable's data folders on macOS, Linux and Windows. Keep the one for your platform.

Every dynotable-cli command opens an agent session, which reaches only the profiles available to agents. dynotable-lsp opens an editor session, which reaches every profile, so the rule denies it.

Neovim and other editors

dynotable-lsp connects an editor that runs a language server over stdio to DynoTable. It is the same program as dynotable-cli, under a second name.

Use it only in an editor, never in an agent. It can use every profile and writes no audit rows.

  • It takes --profile <id or slug> and --no-launch.
  • With no --profile, it uses the profile of the editor's initialization options, then .dynotable.json in the workspace root.
  • The editor must send the language ids sql, partiql and opensearch.
  • It starts DynoTable with no window when the app is not running.

Neovim 0.10 or later:

vim.filetype.add({
  extension = { dtsql = 'sql', partiql = 'partiql', opensearch = 'opensearch' },
})

vim.api.nvim_create_autocmd('FileType', {
  pattern = { 'sql', 'partiql', 'opensearch' },
  callback = function(args)
    local name = vim.api.nvim_buf_get_name(args.buf)
    -- Serve only DynoTable's own SQL files, not every .sql file.
    if vim.bo[args.buf].filetype == 'sql'
      and not (name:match('%.dtsql$') or name:match('%.dynotable%.sql$')) then
      return
    end
    vim.lsp.start({
      name = 'dynotable',
      cmd = { 'dynotable-lsp' },
      root_dir = vim.fs.root(args.buf, { '.dynotable.json', '.git' }),
    }, { bufnr = args.buf })
  end,
})

Other LSP editors can start dynotable-lsp the same way. Only Neovim is tested. For VS Code, use the DynoTable extension.

What an agent can reach

  • Only the DynoTable app on this computer. dynotable-cli connects to nothing else.
  • No AWS credentials. They stay in DynoTable, which runs each request with the profile's own credentials.
  • Only the profiles available to agents. Switch a profile off and the next request fails.
  • Reads only. Writes are staged and committed in the app, by you.
  • A row in the audit log for every data request. Each tables, indices and run request is in the profile's audit log, under Settings → profile → Activity. The row holds the statement, never the rows it returned. When DynoTable cannot write the row, the request fails and returns no data.
  • Nothing while Editor integration is off. Switching it off stops every agent session.

dynotable-cli refuses a DynoTable app older than 1.20.0 with update-app, and no data reaches the agent.

The plugin's diagnostics and completion write no audit rows. They give the agent table, index and field names, never stored values.

A program that can run any command as you can also read DynoTable's files. Give an agent dynotable-cli and nothing broader.

Updated