{"id":9677,"date":"2026-07-15T09:59:51","date_gmt":"2026-07-15T09:59:51","guid":{"rendered":"https:\/\/www.dyalog.com\/blog\/?p=9677"},"modified":"2026-07-15T13:42:46","modified_gmt":"2026-07-15T13:42:46","slug":"working-with-llms-and-dyalog","status":"publish","type":"post","link":"https:\/\/www.dyalog.com\/blog\/2026\/07\/working-with-llms-and-dyalog\/","title":{"rendered":"Working with LLMs and Dyalog"},"content":{"rendered":"<p><em>Notes about this blog post:<\/p>\n<ul>\n<li>In this blog post, I&#8217;m using macOS and <a href=\"https:\/\/www.docker.com\/\">Docker<\/a>. If you&#8217;re a Microsoft Windows user and you want to follow along, Docker works well on <a href=\"https:\/\/learn.microsoft.com\/en-us\/windows\/wsl\/install\">WSL2<\/a>.<\/li>\n<li>AI is a fast-moving field, and I&#8217;ll assume some familiarity with LLMs in terms of terminology.<\/li>\n<\/ul>\n<p><\/em><\/p>\n<p>Working with an AI coding &#8220;agent&#8221; can make us more productive by automating boiler plating, helping with testing, and so on. LLMs are very good at languages like Python and C#, but have struggled with APL. We can speculate as to why this is, but the answer is most likely as mundane as the lack of APL &#8220;out there&#8221;. The training sets are sparse when it comes to APL. The other aspect is one of incentives: no frontier AI laboratory has any real incentive to make their models better at APL; they&#8217;re usually Python shops.<\/p>\n<p>However, the pace of improvement in LLMs generally, and the drive towards &#8220;smarter, not bigger&#8221; models, now make LLMs viable as a productivity tool when working with Dyalog APL. The latest models from <a href=\"https:\/\/www.anthropic.com\/claude\/opus\" target=\"_blank\" rel=\"noopener\">Anthropic<\/a>, <a href=\"https:\/\/openai.com\/index\/introducing-gpt-5-5\/\" target=\"_blank\" rel=\"noopener\">OpenAI<\/a>, and <a href=\"https:\/\/gemini.google.com\/\" target=\"_blank\" rel=\"noopener\">Google<\/a> today write passable APL \u2013 still a bit naive and Python-like, but capable of writing functional, non-trivial APL. In this blog post, I will outline my working practices and set-up, and a few practical tips on how to make an LLM more fluent in APL.<\/p>\n<p><strong>Warning<\/strong>: using an AI coding agent requires care. Although running an AI agent in a container narrows the blast radius, many risks remain unavoidable, especially when running in more autonomous modes.<\/p>\n<h2>&#8220;The November 2025 Inflection Point&#8221;<\/h2>\n<p>Two model releases happened in November, 2025 \u2013 Claude Opus 4.5 and GPT-5.2 \u2013 within days of each other. <a href=\"https:\/\/simonwillison.net\/\" target=\"_blank\" rel=\"noopener\">Simon Willison<\/a> coined the expression <a href=\"https:\/\/simonwillison.net\/2026\/Jan\/4\/inflection\/\" target=\"_blank\" rel=\"noopener\"><em>The November 2025 Inflection Point<\/em><\/a> for this quantum leap. Up until then, LLM performance on APL was abysmal: they were unable to understand the right-to-left execution order and really struggled with the array model in general, let alone syntax. After the November release, all of that changed. Although the models were still not exactly fluent in APL, it was a real step change, especially in their ability to <em>explain<\/em> APL code. APL performance is lifted by the general improvement in models over time \u2013 it remains far behind the performance in more mainstream languages, but is now something that is approaching useful to APL programmers.<\/p>\n<h2>Tooling Improvements<\/h2>\n<p>In conjunction with the model improvements, suddenly &#8220;Agents&#8221; took off. How we interact with LLMs has also evolved, from the original <a href=\"https:\/\/github.com\/features\/copilot\" target=\"_blank\" rel=\"noopener\">CoPilot<\/a> smart auto-complete, to copy-pasting code into the ChatGPT web interface, to chat-window-integrated editors (like the first generation of <a href=\"https:\/\/cursor.com\/\" target=\"_blank\" rel=\"noopener\">Cursor<\/a> or <a href=\"https:\/\/www.windsurf.dev\/\" target=\"_blank\" rel=\"noopener\">Windsurf<\/a>), to today&#8217;s console agents, like Anthropic&#8217;s <a href=\"https:\/\/code.claude.com\/docs\/en\/overview\" target=\"_blank\" rel=\"noopener\">Claude Code<\/a> or OpenAI&#8217;s <a href=\"https:\/\/developers.openai.com\/codex\/cli\" target=\"_blank\" rel=\"noopener\">Codex CLI<\/a>. A console agent is quite a simple idea; it is essentially a loop around an LLM interaction with the ability to call external tools. A console agent&#8217;s tool calling model fits very neatly together with the &#8220;unix philosophy&#8221; of small, composable tools that do one thing. We now have a convenient way of extending the capabilities of AI-powered development tools and, of course, tools like CoPilot and Cursor are now fully &#8220;agentic&#8221; too.<\/p>\n<h2>Evaluating APL<\/h2>\n<p>With tool calling in an agent, we can give an LLM the ability to actually evaluate APL, which, in conjunction with the post-inflection point models, makes a real difference in the productivity you can get from LLMs when working with APL. There are several ways you can achieve this, the simplest being the <code>dyalogscript<\/code> CLI, which has shipped with Dyalog since Dyalog v19.0. Teaching Claude Code how to use <code>dyalogscript<\/code> is remarkably simple \u2013 just make Claude aware of its existence, and tell it to work out how to use it:<\/p>\n<pre class=\"pre-wrap\"><strong>\u276f Figure out how to use \/usr\/local\/bin\/dyalogscript to evaluate APL<\/strong>\r\n\r\n[a few minutes of Claude poking dyalogscript]\r\n\r\n\u25cf  Figured out. Here's how to evaluate APL with \/usr\/local\/bin\/dyalogscript.\r\n\r\n<strong>What it is<\/strong>\r\n\r\ndyalogscript (symlink \u2192 Dyalog 21.0's scriptbin\/dyalogscript, a compiled binary) runs a file of APL source by launching dyalog ... -script FILE. It takes a script file argument \u2014 not an inline expression. (Your existing apl alias just opens the Dyalog 19.0 GUI app; dyalogscript is the headless evaluator.)\r\n\r\n<strong>Basic usage<\/strong>\r\n\r\ndyalogscript path\/to\/script.apls         # run a file\r\ndyalogscript script.apls arg1 arg2       # args land in \u2395ARG\r\ndyalogscript MAXWS=300Mb script.apls     # options go BEFORE the file (or on the shebang)\r\nInside a script: statements are separated by newlines or \u22c4, output with \u2395\u2190, and strings use single quotes ('hi', not \"hi\").\r\n\r\n<strong>\u26a0\ufe0f The one real gotcha: output uses \\r, not \\n<\/strong>\r\n\r\n\u2395\u2190 terminates each line with a carriage return, so in a terminal\/pipe lines overwrite each other and look concatenated (455Hello\u2026). Fix by normalizing:\r\ndyalogscript script.apls | tr '\\r' '\\n'\r\n\r\n<strong>Ad-hoc evaluation without a temp file<\/strong>\r\n\r\nAll of these work because the wrapper only needs a readable file path:\r\n# pipe via \/dev\/stdin (most portable)\r\nprintf '\u2395\u2190+\/\u237310\\n' | dyalogscript \/dev\/stdin | tr '\\r' '\\n'\r\n\r\n# heredoc\r\ndyalogscript \/dev\/stdin &lt;&lt;'APL' | tr '\\r' '\\n'\r\n\u2395\u21902+2\r\n\u2395\u21903 3\u2374\u23739\r\nAPL\r\n\r\n# process substitution\r\ndyalogscript &lt;(printf '\u2395\u2190\u233d\u23735\\n') | tr '\\r' '\\n'\r\nOr make a script self-executable with a shebang (as the samples in \u2026\/Dyalog\/Samples\/scripts\/ do):\r\n#!\/usr\/local\/bin\/dyalogscript MAXWS=64Mb\r\n\u2395\u2190'hello'\r\n\r\n<strong>Errors<\/strong>\r\n\r\nUncaught errors print inline (error type, the offending line, and a \u2227 caret) and halt the script. Note: exit code is always 0, even on error \u2014 so detect failures by scanning output, not $? (wrap risky code in :Trap\/\u2395TRAP if you need control flow).\r\n<\/pre>\n<p>Well done, Claude (despite the hallucination of <code>\u2395ARG<\/code> in a comment). However, doing this every time you start Claude soon gets tedious. Fortunately, Claude Code has a mechanism for encoding such things durably: skills. A &#8220;Claude skill&#8221; is a piece of Markdown prose that encodes some trigger condition with a brief description of the tool or behaviour that it&#8217;s concerned with. You can view\/download my <a href=\"https:\/\/github.com\/dyalog-labs\/agent-dev-container\/blob\/main\/.devcontainer\/kit\/.claude\/skills\/dyalog-script\/SKILL.md\">dyalogscript skill<\/a> from the <a href=\"https:\/\/github.com\/dyalog-labs\">Dyalog Labs GitHub<\/a>, or you could tell Claude to write one for you after it&#8217;s worked out how to use it.<\/p>\n<p>Let&#8217;s ask Claude to pick up the skills file:<\/p>\n<pre class=\"pre-wrap\"><strong>\u276f Grab the skill from https:\/\/github.com\/dyalog-labs\/agent-dev-container\/blob\/main\/.devcontainer\/kit\/.claude\/skills\/dyalog-script\/SKILL.md<\/strong>\r\n\r\n\u25cf  I'll grab the raw file. Let me fetch it from the raw GitHub URL.\r\n\r\n\u25cf  Bash(curl -fsSL \"https:\/\/... [snip]\")\r\n\r\n\u25cf  Got it \u2014 a self-contained skill. Let me check whether the skill directory has any sibling files (scripts\/resources) I should grab too.\r\n\r\n\u25cf  Bash(curl -fsSL \"https:\/\/... [snip]\")\r\n\r\n\u25cf  Installed and verified \u2014 1279 bytes, an exact match for GitHub's reported size, so it's byte-for-byte identical and the APL glyphs survived intact.\r\n\r\n  Location: .claude\/skills\/dyalog-script\/SKILL.md (it was the only file in that skill's directory \u2014 nothing else to grab).\r\n\r\n  A few notes:\r\n\r\n  - Availability: Skills load at startup, so dyalog-script will show up as an invocable skill in your next Claude Code session, not this one.\r\n  - Scope: I installed it at project-local scope (\/.claude\/skills\/), so it's available in this project. Say the word if you'd rather have it user-scope. (~\/.claude\/skills\/).\r\n  - Content: It matches exactly what we worked out last turn \u2014 dyalogscript \/dev\/stdin with echo\/heredoc, and \u2395\u2190 to print. One thing the skill doesn't mention that we discovered: on this macOS install, \u2395\u2190 separates lines with \\r, so for clean multi-line terminal output you may still want to pipe through tr '\\r' '\\n'.\r\n<\/pre>\n<p>We need to restart Claude Code for it to pick it up. After restart, hitting <code>\/<\/code> shows the available skills and commands, and we should see our fresh dyalogscript skill:<\/p>\n<div id=\"attachment_9685\" style=\"width: 2812px\" class=\"wp-caption aligncenter\"><img loading=\"lazy\" decoding=\"async\" aria-describedby=\"caption-attachment-9685\" class=\"size-full wp-image-9685\" src=\"https:\/\/www.dyalog.com\/blog\/wp-content\/uploads\/2026\/07\/workingwithllms_claude1.png\" alt=\"Claude Code showing the available skills and commands\" width=\"2802\" height=\"912\" srcset=\"https:\/\/www.dyalog.com\/blog\/wp-content\/uploads\/2026\/07\/workingwithllms_claude1.png 2802w, https:\/\/www.dyalog.com\/blog\/wp-content\/uploads\/2026\/07\/workingwithllms_claude1-300x98.png 300w, https:\/\/www.dyalog.com\/blog\/wp-content\/uploads\/2026\/07\/workingwithllms_claude1-768x250.png 768w, https:\/\/www.dyalog.com\/blog\/wp-content\/uploads\/2026\/07\/workingwithllms_claude1-1024x333.png 1024w, https:\/\/www.dyalog.com\/blog\/wp-content\/uploads\/2026\/07\/workingwithllms_claude1-500x163.png 500w\" sizes=\"auto, (max-width: 2802px) 100vw, 2802px\" \/><p id=\"caption-attachment-9685\" class=\"wp-caption-text\">Claude Code showing the available skills and commands<\/p><\/div>\n<p>Claude should now be able to run APL expressions by itself. Let&#8217;s test that (note that the apparently mangled lines are due to the line break issues mentioned above):<\/p>\n<pre class=\"pre-wrap\"><strong>\u276f Evaluate the following Dyalog APL expression and tell me how it works: 3 (\u2374\u23741,0\u2374\u2368\u22a2+.*\u2218\u2373-\u2218\u2262) 4<\/strong>\r\n\r\n\u25cf  I'll analyze this expression carefully, then verify my reasoning by running it.\r\n\r\n  Let me first work through the structure, then check it against the interpreter.\r\n\r\n\u25cf  Skill(dyalog-script)\r\n  \u23bf  Successfully loaded skill\r\n\r\n\u25cf  Bash(dyalogscript \/dev\/stdin &lt;&lt;'EAPL'\r\n      \u2395\u2190'=== full result ==='\u2026)\r\n  \u23bf  [tool calling log removed for brevity]\r\n\r\n\u25cf  Bash(dyalogscript \/dev\/stdin &lt;&lt;'EAPL'\r\n      \u235d break down the inner tines with X=3 (left), Y=4 (right)\u2026)\r\n  \u23bf  [tool calling log removed for brevity] \r\n\r\n\u25cf  Confirmed. The result is a 4\u00d74\u00d74 identity tensor \u2014 a rank-3 array with 1s on the main diagonal (where all three indices are equal) and 0s everywhere else:\r\n\r\n1 0 0 0      0 0 0 0      0 0 0 0      0 0 0 0\r\n0 0 0 0      0 1 0 0      0 0 0 0      0 0 0 0\r\n0 0 0 0      0 0 0 0      0 0 1 0      0 0 0 0\r\n0 0 0 0      0 0 0 0      0 0 0 0      0 0 0 1\r\n\r\n<strong>What it really is<\/strong>\r\n\r\nThis is a generalized identity-array generator. The left argument is the rank (3) and the right argument is the dimension (4), so 3 (\u2026) 4 builds the 4\u00d74\u00d74 identity. (2 (\u2026) 4 would give an ordinary 4\u00d74 identity matrix.)\r\n\r\n<strong>How it works<\/strong>\r\n\r\nThe parenthesised part is a train (tacit function). Operators bind tighter than the train, so first the derived functions form: +.*\u2218\u2373 is (+.*)\u2218\u2373, and -\u2218\u2262 is (-)\u2218(\u2262). That\r\nleaves 9 tines, which\u2014being odd-length\u2014nest as right-grouped forks:\r\n\r\n(\u2374 \u2374 (1 , (0 \u2374\u2368 (\u22a2 +.*\u2218\u2373 -\u2218\u2262))))\r\n\r\nEvaluating with X\u21903 (rank r), Y\u21904 (dimension n), inside-out:\r\n\r\nInnermost fork (\u22a2 +.*\u2218\u2373 -\u2218\u2262) \u2014 a fork applies as (X f Y) g (X h Y):\r\n- Left tine 3\u22a24 \u2192 4 (the dimension n)\r\n- Right tine 3(-\u2218\u2262)4 \u2192 3 - \u22624 \u2192 3 - 1 \u2192 2 (this is r-1, the top exponent; \u2262 of a scalar is 1)\r\n- Middle 4 (+.*\u2218\u2373) 2 \u2192 4 +.* \u23732 \u2192 4 +.* 1 2 \u2192 4^1 + 4^2 \u2192 20\r\n\r\n- That inner product computes \u03a3 n^k for k=1\u2026r-1 = 4 + 16 = the number of zeros needed.\r\n\r\n0 \u2374\u2368 20 \u2192 20 \u2374 0 \u2192 twenty 0s.\r\n\r\n1 , \u2026 \u2192 1 0 0 \u2026 0, length 21. This is the key: the vector's period is 1 + n + n\u00b2= 21.\r\n\r\nOuter fork (\u2374 \u2374 \u2026):\r\n- Left tine 3\u23744 \u2192 4 4 4 (the shape: r copies of n)\r\n- Reshape: 4 4 4 \u2374 (1,20\u23740)\r\n\r\nReshaping recycles the 21-element vector across 64 cells. The flat index of each diagonal element (i,i,i) is i \u00d7 (1+n+n\u00b2) = i \u00d7 21, which lands exactly on the recycled 1s (flat indices 0, 21, 42, 63) \u2014 producing the identity tensor.\r\n\r\n<strong>The idea in one line<\/strong>\r\n\r\nIt's the classic identity-matrix idiom n n \u2374 1, n\u23740 lifted to arbitrary rank: the period (n\u02b3\u22121)\/(n\u22121) = 1+n+\u2026+n\u02b3\u207b\u00b9 makes a single 1 recur precisely on the main diagonal, and the train computes that period tacitly from the rank\/dimension arguments.\r\n\r\n<strong>\u273b Crunched for 2m 24s<\/strong><\/pre>\n<p>From this, we can note several things. Firstly, Claude worked out that it needed to load and invoke the <code>dyalogscript<\/code> skill, and secondly, it did a decent job of dissecting the fairly complex tacit expression for arbitrary rank identity matrix. It approached the job methodically, and tested as it progressed through the process.<\/p>\n<p>At the time of writing, trying the same expression with the latest <a href=\"https:\/\/chatgpt.com\/\" target=\"_blank\" rel=\"noopener\">ChatGPT<\/a> without the ability to evaluate returns a confident, but unfortunately nonsensical answer.<\/p>\n<p>&#8220;Skills&#8221; is now an <a href=\"https:\/\/agentskills.io\/home\" target=\"_blank\" rel=\"noopener\">open standard<\/a>, and OpenAI supports it, too. If I&#8217;d given OpenAI&#8217;s Codex tool the same skill, it would have solved it, too.<\/p>\n<p>If you only take one thing from this blog post, then it should be this: enabling APL evaluation unlocks a lot of APL ability in the latest models.<\/p>\n<h4>Stateful APL evaluation<\/h4>\n<p><code>dyalogscript<\/code> isn&#8217;t stateful \u2013 it runs an expression or file, and then exits. This doesn&#8217;t help you if you are working in a large, complex, APL application. For that you need a different mechanism, either connecting using the Ride port, or using an editor extension for something like VS Code. We&#8217;re currently working on such solutions, but that is out of scope for this blog post.<\/p>\n<h2 id=\"containerising-for-better-safety\">Containerising for Improved Safety<\/h2>\n<p>Safety when using AI is an important topic, and too big to do justice in a blog post like this. Running an agent locally, directly on your machine, exposes you to real risk: the agent can read, write, and delete files, install software, access credentials, access the local network and, of course, the web. Although the agents from reputable AI providers generally have both a good track record and plenty of internal guardrails, the risks are real. So what can you do if you want to experiment with AI agents whilst at the same time taking steps to minimise your exposure? One way is to run the agent in a container; this alone doesn&#8217;t mean &#8220;safe&#8221;, but it should at least decrease the blast radius.<\/p>\n<p>I run Claude Code in a <a href=\"https:\/\/code.visualstudio.com\/docs\/devcontainers\/containers\" target=\"_blank\" rel=\"noopener\">dev container<\/a>, a <a href=\"https:\/\/www.docker.com\/\" target=\"_blank\" rel=\"noopener\">Docker<\/a> container that is configured to be seamlessly picked up by code editors like <a href=\"https:\/\/code.visualstudio.com\/\" target=\"_blank\" rel=\"noopener\">VS Code<\/a> and <a href=\"https:\/\/zed.dev\/\" target=\"_blank\" rel=\"noopener\">Zed<\/a>. We don&#8217;t publish a built container image, nor do we support it, but you can see and use its source code on <a href=\"https:\/\/github.com\/dyalog-labs\/\" target=\"_blank\" rel=\"noopener\">Dyalog Labs<\/a>, a GitHub organisation that we use at Dyalog Ltd specifically for experimenting with potentially-useful things that haven&#8217;t yet reached the standard for an &#8220;officially supported product&#8221;. The relevant repository is <a href=\"https:\/\/github.com\/dyalog-labs\/agent-dev-container\" target=\"_blank\" rel=\"noopener\">agent-dev-container<\/a>\u00a0\u2013 make sure that you examine it closely before you decide to make use of it.<\/p>\n<p>Running Claude in the container means that it can see only the directory in which it was started and those below it. This container comes with an optional &#8220;starter kit&#8221; Claude code configuration and set-up \u2013 you already saw the <code>dyalogscript<\/code> skill. Let&#8217;s explore some of its features.<\/p>\n<p>Start by cloning the dev container repository and lifting its configuration into our project repository:<\/p>\n<pre><code>~\/work\/tmp  $ git clone git@github.com:dyalog-labs\/agent-dev-container.git\r\nCloning into 'agent-dev-container'...\r\nremote: Enumerating objects: 67, done.\r\nremote: Counting objects: 100% (67\/67), done.\r\nremote: Compressing objects: 100% (50\/50), done.\r\nremote: Total 67 (delta 21), reused 55 (delta 12), pack-reused 0 (from 0)\r\nReceiving objects: 100% (67\/67), 55.42 KiB | 2.52 MiB\/s, done.\r\nResolving deltas: 100% (21\/21), done.\r\n~\/work\/tmp  $ mkdir my-project\r\n~\/work\/tmp  $ cd my-project\r\n~\/work\/tmp\/my-project  $ cp -r ..\/agent-dev-container\/.devcontainer .\r\n~\/work\/tmp\/my-project  $ code .<\/code><\/pre>\n<p>Opening the directory with VS Code lets it recognise that it contains a dev container and offer to open it in container mode. The first time we do this we trigger the container build, which can take several minutes to complete:<\/p>\n<div id=\"attachment_9686\" style=\"width: 3004px\" class=\"wp-caption aligncenter\"><img loading=\"lazy\" decoding=\"async\" aria-describedby=\"caption-attachment-9686\" class=\"size-full wp-image-9686\" src=\"https:\/\/www.dyalog.com\/blog\/wp-content\/uploads\/2026\/07\/workingwithllms_devcontainer1.png\" alt=\"First open of dev container\" width=\"2994\" height=\"2118\" srcset=\"https:\/\/www.dyalog.com\/blog\/wp-content\/uploads\/2026\/07\/workingwithllms_devcontainer1.png 2994w, https:\/\/www.dyalog.com\/blog\/wp-content\/uploads\/2026\/07\/workingwithllms_devcontainer1-300x212.png 300w, https:\/\/www.dyalog.com\/blog\/wp-content\/uploads\/2026\/07\/workingwithllms_devcontainer1-768x543.png 768w, https:\/\/www.dyalog.com\/blog\/wp-content\/uploads\/2026\/07\/workingwithllms_devcontainer1-1024x724.png 1024w, https:\/\/www.dyalog.com\/blog\/wp-content\/uploads\/2026\/07\/workingwithllms_devcontainer1-424x300.png 424w\" sizes=\"auto, (max-width: 2994px) 100vw, 2994px\" \/><p id=\"caption-attachment-9686\" class=\"wp-caption-text\">First open of dev container<\/p><\/div>\n<p>Open the terminal pane, and run the kit installation command to surface the Claude Code configuration (by keeping the dev container itself separate from the Claude configuration, you can choose to use either or both):<\/p>\n<div id=\"attachment_9687\" style=\"width: 3004px\" class=\"wp-caption aligncenter\"><img loading=\"lazy\" decoding=\"async\" aria-describedby=\"caption-attachment-9687\" class=\"size-full wp-image-9687\" src=\"https:\/\/www.dyalog.com\/blog\/wp-content\/uploads\/2026\/07\/workingwithllms_devcontainer2.png\" alt=\"Install Claude &quot;kit&quot;\" width=\"2994\" height=\"2118\" srcset=\"https:\/\/www.dyalog.com\/blog\/wp-content\/uploads\/2026\/07\/workingwithllms_devcontainer2.png 2994w, https:\/\/www.dyalog.com\/blog\/wp-content\/uploads\/2026\/07\/workingwithllms_devcontainer2-300x212.png 300w, https:\/\/www.dyalog.com\/blog\/wp-content\/uploads\/2026\/07\/workingwithllms_devcontainer2-768x543.png 768w, https:\/\/www.dyalog.com\/blog\/wp-content\/uploads\/2026\/07\/workingwithllms_devcontainer2-1024x724.png 1024w, https:\/\/www.dyalog.com\/blog\/wp-content\/uploads\/2026\/07\/workingwithllms_devcontainer2-424x300.png 424w\" sizes=\"auto, (max-width: 2994px) 100vw, 2994px\" \/><p id=\"caption-attachment-9687\" class=\"wp-caption-text\">Install Claude &#8220;kit&#8221;<\/p><\/div>\n<p>Now we can start Claude Code. As we&#8217;re in the container, we can enable the ominously named <code>--dangerously-skip-permissions<\/code> mode:<\/p>\n<div id=\"attachment_9688\" style=\"width: 3004px\" class=\"wp-caption aligncenter\"><img loading=\"lazy\" decoding=\"async\" aria-describedby=\"caption-attachment-9688\" class=\"size-full wp-image-9688\" src=\"https:\/\/www.dyalog.com\/blog\/wp-content\/uploads\/2026\/07\/workingwithllms_devcontainer3.png\" alt=\"Start in yolo mode\" width=\"2994\" height=\"2118\" srcset=\"https:\/\/www.dyalog.com\/blog\/wp-content\/uploads\/2026\/07\/workingwithllms_devcontainer3.png 2994w, https:\/\/www.dyalog.com\/blog\/wp-content\/uploads\/2026\/07\/workingwithllms_devcontainer3-300x212.png 300w, https:\/\/www.dyalog.com\/blog\/wp-content\/uploads\/2026\/07\/workingwithllms_devcontainer3-768x543.png 768w, https:\/\/www.dyalog.com\/blog\/wp-content\/uploads\/2026\/07\/workingwithllms_devcontainer3-1024x724.png 1024w, https:\/\/www.dyalog.com\/blog\/wp-content\/uploads\/2026\/07\/workingwithllms_devcontainer3-424x300.png 424w\" sizes=\"auto, (max-width: 2994px) 100vw, 2994px\" \/><p id=\"caption-attachment-9688\" class=\"wp-caption-text\">Start in yolo mode<\/p><\/div>\n<p>The first time that you need to log in to Claude, follow the instructions to land at an authenticated Claude session. If you have a Team subscription to Claude, <kbd>Shift<\/kbd> + <kbd>Tab<\/kbd> until you reach <code>auto<\/code> mode, which is the permissions mode that you want to be running in (keeping the <code>bypass permissions<\/code> mode for the rare occasions when you really need to escalate permissions).<\/p>\n<div id=\"attachment_9689\" style=\"width: 3004px\" class=\"wp-caption aligncenter\"><img loading=\"lazy\" decoding=\"async\" aria-describedby=\"caption-attachment-9689\" class=\"size-full wp-image-9689\" src=\"https:\/\/www.dyalog.com\/blog\/wp-content\/uploads\/2026\/07\/workingwithllms_devcontainer4.png\" alt=\"Claude running\" width=\"2994\" height=\"2118\" srcset=\"https:\/\/www.dyalog.com\/blog\/wp-content\/uploads\/2026\/07\/workingwithllms_devcontainer4.png 2994w, https:\/\/www.dyalog.com\/blog\/wp-content\/uploads\/2026\/07\/workingwithllms_devcontainer4-300x212.png 300w, https:\/\/www.dyalog.com\/blog\/wp-content\/uploads\/2026\/07\/workingwithllms_devcontainer4-768x543.png 768w, https:\/\/www.dyalog.com\/blog\/wp-content\/uploads\/2026\/07\/workingwithllms_devcontainer4-1024x724.png 1024w, https:\/\/www.dyalog.com\/blog\/wp-content\/uploads\/2026\/07\/workingwithllms_devcontainer4-424x300.png 424w\" sizes=\"auto, (max-width: 2994px) 100vw, 2994px\" \/><p id=\"caption-attachment-9689\" class=\"wp-caption-text\">Claude running<\/p><\/div>\n<p>This dev container comes equipped with development tool chains for Dyalog v20.0, C# .NET 8, Python 3.11, Go 1.24.1, and Node v20.20.2. It has the <code>dyalogscript<\/code> skill we showed earlier:<\/p>\n<div id=\"attachment_9684\" style=\"width: 3004px\" class=\"wp-caption aligncenter\"><img loading=\"lazy\" decoding=\"async\" aria-describedby=\"caption-attachment-9684\" class=\"size-full wp-image-9684\" src=\"https:\/\/www.dyalog.com\/blog\/wp-content\/uploads\/2026\/07\/workingwithllms_devcontainer5.png\" alt=\"Claude running\" width=\"2994\" height=\"2118\" srcset=\"https:\/\/www.dyalog.com\/blog\/wp-content\/uploads\/2026\/07\/workingwithllms_devcontainer5.png 2994w, https:\/\/www.dyalog.com\/blog\/wp-content\/uploads\/2026\/07\/workingwithllms_devcontainer5-300x212.png 300w, https:\/\/www.dyalog.com\/blog\/wp-content\/uploads\/2026\/07\/workingwithllms_devcontainer5-768x543.png 768w, https:\/\/www.dyalog.com\/blog\/wp-content\/uploads\/2026\/07\/workingwithllms_devcontainer5-1024x724.png 1024w, https:\/\/www.dyalog.com\/blog\/wp-content\/uploads\/2026\/07\/workingwithllms_devcontainer5-424x300.png 424w\" sizes=\"auto, (max-width: 2994px) 100vw, 2994px\" \/><p id=\"caption-attachment-9684\" class=\"wp-caption-text\">Claude running<\/p><\/div>\n<p>and the ability to read Dyalog&#8217;s documentation:<\/p>\n<pre class=\"pre-wrap\"><strong>\u276f Show me the Dyalog documentation for \u2395FIX.<\/strong>                                                                                      \r\n\r\n\u25cf Skill(dyalog-docsearch)\r\n  \u23bf  Successfully loaded skill\r\n\r\n\u25cf Bash(docsearch -s '\u2395FIX' -l 5)\r\n  \u23bf  313 Fix Script {R}\u2190{X}\u2395FIX Y\r\n     28 Fix Script {R}\u2190{X}\u2395FIX Y\r\n     332 Fix Definition {R}\u2190\u2395FX Y\r\n     \u2026 +2 lines (ctrl+o to expand)\r\n  \u23bf  Allowed by auto mode classifier\r\n\r\n\u25cf Bash(docsearch -r 313)\r\n  \u23bf  # Fix Script {R}\u2190{X}\u2395FIX Y\r\n     \u2026 +306 lines (ctrl+o to expand)\r\n  \u23bf  Allowed by auto mode classifier\r\n\r\n\u25cf Here's the Dyalog documentation for \u2395FIX.\r\n\r\n  <strong>{R}\u2190{X}\u2395FIX Y \u2014 Fix Script<\/strong>\r\n\r\n  \u2395FIX establishes namespaces, classes, interfaces and functions from a script into the workspace. (In this entry, \"namespace\"\r\n  covers scripted Namespaces, Classes and Interfaces.)\r\n\r\n  Right argument Y\r\n\r\n  - A vector of character vectors\/scalars representing a script, or\r\n  - A simple character vector starting with file:\/\/ followed by an existing file name (relative or absolute; \/ recommended as the\r\n  delimiter for portability, though \\ also works on Windows). The file contents follow the same rules as the script form.\r\n\r\n[snipped...]\r\n\r\n<strong>\u273b Saut\u00e9ed for 30s<\/strong><\/pre>\n<h2>Defence in Depth: hooks<\/h2>\n<p>Emphasising again that containerising is necessary but not sufficient, the dev container features a set of Claude Code &#8220;hooks&#8221;, which are security guardrails guaranteed to run at certain set points of the Claude Code life cycle. You can see the included hooks in the <a href=\"https:\/\/github.com\/dyalog-labs\/agent-dev-container\/blob\/main\/.devcontainer\/kit\/.claude\/hooks\/\" target=\"_blank\" rel=\"noopener\">GitHub repository<\/a>. For example, these hooks stop Claude from peeking into any <strong>.env<\/strong> files (which are often used for credentials):<\/p>\n<pre class=\"pre-wrap\"><strong>\u276f Tell me the value of the key FOO in .env<\/strong>                                                                                                  \r\n\r\n  Searched for 1 pattern, read 1 file (ctrl+o to expand)\r\n\r\n\u25cf I can't read .env. Both paths to it are blocked by this project's guardrails:\r\n\r\n  - The Bash hook (block-dangerous-bash.sh) blocks any command touching .env.\r\n  - The Read tool reports .env is in a permission-denied directory.\r\n\r\n  .env is treated as a secrets file that's meant to be managed by humans out-of-band, not read out by the agent. So I can't retrieve the value of FOO for you.\r\n\r\n  If you genuinely need it, you can check it yourself:\r\n\r\n  ! grep '^FOO=' .env\r\n  - The Bash hook (block-dangerous-bash.sh) blocks any command touching .env.\r\n  - The Read tool reports .env is in a permission-denied directory.\r\n\r\n  .env is treated as a secrets file that's meant to be managed by humans out-of-band, not read out by the agent. So I can't retrieve the value of FOO for you.\r\n\r\n<strong>\u273b Cogitated for 13s<\/strong><\/pre>\n<p>Note that hooks protect against accidents, not malice. Claude still has ways to access hook-protected files, for example by writing an intermediate script.<\/p>\n<h2>In Conclusion&#8230;<\/h2>\n<p>If you have tried to use LLMs with APL before and been disappointed, it&#8217;s time to take another look. Although we&#8217;re still lagging behind languages like Python and JavaScript, the newest models and agent tools, in conjunction with the ability to evaluate APL, unlock a lot of latent fluency, resulting in a real step forward. Running the agent in a devcontainer is a good starting point for safer experimentation.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Notes about this blog post: In this blog post, I&#8217;m using macOS and Docker. If you&#8217;re a Microsoft Windows user and you want to follow along, Docker works well on WSL2. AI is a fast-moving field, and I&#8217;ll assume some &hellip; <a href=\"https:\/\/www.dyalog.com\/blog\/2026\/07\/working-with-llms-and-dyalog\/\">Continue reading <span class=\"meta-nav\">&rarr;<\/span><\/a><\/p>\n","protected":false},"author":39,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[18,19],"tags":[],"class_list":["post-9677","post","type-post","status-publish","format-standard","hentry","category-tips-tricks","category-tools"],"_links":{"self":[{"href":"https:\/\/www.dyalog.com\/blog\/wp-json\/wp\/v2\/posts\/9677","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.dyalog.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.dyalog.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.dyalog.com\/blog\/wp-json\/wp\/v2\/users\/39"}],"replies":[{"embeddable":true,"href":"https:\/\/www.dyalog.com\/blog\/wp-json\/wp\/v2\/comments?post=9677"}],"version-history":[{"count":57,"href":"https:\/\/www.dyalog.com\/blog\/wp-json\/wp\/v2\/posts\/9677\/revisions"}],"predecessor-version":[{"id":9746,"href":"https:\/\/www.dyalog.com\/blog\/wp-json\/wp\/v2\/posts\/9677\/revisions\/9746"}],"wp:attachment":[{"href":"https:\/\/www.dyalog.com\/blog\/wp-json\/wp\/v2\/media?parent=9677"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.dyalog.com\/blog\/wp-json\/wp\/v2\/categories?post=9677"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.dyalog.com\/blog\/wp-json\/wp\/v2\/tags?post=9677"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}