[{"content":"","permalink":"https://cashwilliams.com/radio/not-yours-to-lose/","summary":"","title":"Not Yours to Lose"},{"content":"","permalink":"https://cashwilliams.com/radio/one-more-run/","summary":"","title":"One More Run"},{"content":"There’s a habit in AI tooling circles where every problem somehow ends at MCP.\nNeed tool use? MCP. Need integrations? MCP. Need agents to touch real systems? Obviously MCP.\nI think that instinct is backwards.\nFor a lot of real-world agent work, Skills are the better pattern. Not more elegant on a whiteboard. Better when you actually have to build, run, debug, and live with the thing.\nThe reason is simple: we already have a pattern for this, and it has been working for decades.\nLinux was built on small tools that do one thing well. That pattern survived because it works. It’s composable, inspectable, debuggable, and cheap to extend. Skills fit that shape naturally. MCPs usually don’t.\nThe CLI Pattern Already Works I don’t need a new protocol to believe in tools. I already have one. It’s called the command line.\nA clean CLI gives me almost everything I want for agent work:\na stable interface obvious inputs and outputs scriptability composability versioning easy local testing easy remote execution natural logging and debugging If a tool has a sane command structure, useful --help, and a --json mode, an LLM can usually drive it just fine.\nPeople keep underrating that.\nModern models are already very good at bash. Coding agents are especially good at it. Give them a clean tool surface and a few examples and they get to work fast.\nSo the winning pattern is often not “invent a protocol layer.” It’s:\nbuild a small tool make the output clean write a short Skill explaining best use ship it That’s boring. Good. Boring wins.\nSkills Are Lightweight in Exactly the Right Way A Skill is not magic. It’s not trying to be.\nA good Skill is just a compact operator manual for the model:\nuse this tool for these jobs here are the commands that matter prefer these flags request JSON here are the gotchas don’t do the dumb thing That is a very efficient way to teach an agent how to work.\nIt also matches how humans already use software. We do not need every tool wrapped in a universal capability bus before it becomes usable. Sometimes you just need a solid binary and a page of instructions.\nUnix figured this out a long time ago.\nMCPs Add More Machinery Than Most Jobs Need This is my real issue with MCP.\nIt turns a lot of otherwise simple integrations into infrastructure projects.\nNow the tool has to live somewhere. You need to run a server locally, in Docker, through npx, or host it remotely. You’ve got another process, another lifecycle, another auth path, another thing to break.\nMeanwhile a CLI can just use the API directly.\nThat’s a huge difference.\nIf I want an agent to interact with a service, I can usually build a focused CLI wrapper in Go or Python much faster than I can design, expose, and maintain a polished MCP server. The CLI is also easier to test, easier to log, and easier to reason about.\nThat matters when you’re trying to ship instead of admire architecture diagrams.\nContext Is a Budget, and MCP Spends It Fast Another thing people gloss over: tool definitions are not free.\nMCPs tend to come with a bunch of schema, descriptions, argument surfaces, and capability metadata that have to be exposed to the model. Add enough of them and you start burning a meaningful amount of context just on tool plumbing.\nThat is stupidly expensive if the model only needs a small slice of those tools.\nSkills are usually much lighter.\nTen Skills can have almost no impact if you load them selectively. Ten chunky MCP integrations can start eating context fast, especially when the tool definitions get verbose.\nAnd context is working memory. Burn enough of it on plumbing and you make the model worse at the actual job.\nCoding Agents Changed the Equation This point matters more now than it did even a few months ago.\nToday, coding agents can create small purpose-built CLIs ridiculously fast.\nNeed to wrap one API? Need a tool that normalizes auth, emits JSON, and exposes three subcommands? Need it in Go so it compiles to one static binary and ships everywhere?\nThat’s not a giant project anymore. That’s often an afternoon.\nAnd once you have that CLI, you have a tool that is:\neasier for humans to inspect easier for agents to drive easier to run in CI easier to test locally easier to compose with other tools That’s a better artifact than an MCP server in a surprising number of cases.\nNot because protocols are secretly evil, but because narrow tools are often better than broad abstractions.\nSkills Encode Judgment, Not Just Capability This is the part I think protocol people consistently underrate.\nAccess is not the hard part. Judgment is.\nThe problem is rarely “can the model technically call a function?” The problem is “does the model know which tool to use, when to use it, how to use it safely, and which weird edge cases matter?”\nThat is exactly what Skills are good at.\nA Skill can encode things like:\nalways use --json batch writes instead of spamming one-item updates don’t trust the default date parser use the read command before the write command this tool is safe for reads but not for destructive actions this API lies unless you pass the verbose flag That kind of guidance is where real reliability comes from.\nMCP exposes capability. Skills teach judgment.\nAnd if you’ve spent any time around automation, you know judgment is carrying a lot of the system.\nCLI Workflows Fail in More Honest Ways Another point in favor of Skills plus CLIs: when things break, they usually break in ways you can actually understand.\ncommand not found bad flag auth failed API returned an error output malformed process exited nonzero That’s fine. I can work with that.\nThe more layers you add between the agent and the actual system, the more you create weird failure modes that have nothing to do with the job itself. That’s how you lose half a day debugging plumbing instead of getting useful work done.\nI like systems that fail loudly and plainly.\nThe shell is very good at that.\nThis Also Fits How Teams Already Work This is the least sexy argument, which probably means it’s one of the strongest.\nMost teams already know how to work with CLIs.\nThey know how to:\ninstall them version them wrap them pin them run them in CI capture stdout parse JSON debug failures wire them into scripts and workflows That existing muscle matters.\nSkills fit into that world cleanly. They don’t ask you to stop using the patterns that already made software manageable. They give the model the same kind of sharp, explicit guidance you’d give a human operator.\nMy Default Pattern Now If I’m building tooling for an agent today, my default instinct is:\nmake a small CLI keep it narrow support --help support --json make errors obvious write a short Skill for best practices That stack is fast, cheap, legible, and durable.\nIt’s also friendly to both humans and agents, which matters more than people think. The best tools are usually the ones both can use without needing a priesthood.\nStop Reaching for MCP First This is really the whole argument.\nMCP is not the default answer. It’s an extra layer, and extra layers should justify themselves.\nA lot of the time they don’t.\nA good CLI plus a good Skill is often the more practical design:\nless infrastructure less context bloat faster to build easier to test easier to debug easier to replace better aligned with how LLMs already operate That is a hell of a lot of wins for a pattern people keep treating like it’s second-class.\nIt isn’t second-class. It’s just less fashionable.\nAnd I trust the boring pattern that already built half of modern computing more than the shiny one that keeps turning every simple integration into a protocol debate.\nIf you’re building agent systems right now, I’d start here:\nWhat is the smallest useful tool I can give the model, and what short set of instructions will make it use that tool well?\nMost of the time, the answer is not an MCP server. It’s a Skill and a clean CLI.\n","permalink":"https://cashwilliams.com/why-skills-beat-mcp/","summary":"\u003cp\u003eThere’s a habit in AI tooling circles where every problem somehow ends at MCP.\u003c/p\u003e\n\u003cp\u003eNeed tool use? MCP. Need integrations? MCP. Need agents to touch real systems? Obviously MCP.\u003c/p\u003e\n\u003cp\u003eI think that instinct is backwards.\u003c/p\u003e\n\u003cp\u003eFor a lot of real-world agent work, Skills are the better pattern. Not more elegant on a whiteboard. Better when you actually have to build, run, debug, and live with the thing.\u003c/p\u003e\n\u003cp\u003eThe reason is simple: we already have a pattern for this, and it has been working for decades.\u003c/p\u003e","title":"Why Skills Beat MCP"},{"content":"Early in OpenClaw days (it was called Clawdis at the time, not even Clawdbot yet), everyone was noticing token spend was much higher than expected. I wanted to debug, so needed a way to see what was actually happening. How long is the system prompt? How many API calls per conversation? What\u0026rsquo;s the token breakdown?\nI\u0026rsquo;ve used the LiteLLM -\u0026gt; LangFuse pattern before, so I quickly setup OpenClaw to point to a new instance of it. These are rough notes on how to do that.\nThe Stack LiteLLM is an open source proxy that sits between your agent and the LLM providers. One API endpoint, any model. It handles routing, fallbacks, and logging.\nLangFuse is an open source observability platform for LLM apps. It captures traces, visualizes token usage, and tracks costs over time. LiteLLM has native support to send to LangFuse.\nTogether, the architecture looks like this:\n┌─────────────┐ ┌─────────────┐ ┌─────────────────┐ │ OpenClaw │────▶│ LiteLLM │────▶│ Model APIs │ │ Agent │ │ Proxy │ │ (Anthropic, │ └─────────────┘ └──────┬──────┘ │ OpenAI, etc.) │ │ └─────────────────┘ │ ▼ ┌─────────────┐ │ LangFuse │ │ (traces) │ └─────────────┘ OpenClaw calls LiteLLM instead of hitting provider APIs directly. LiteLLM routes to the right provider and sends trace data to LangFuse.\nLiteLLM log display showing recent LLM calls.\nSetting It Up If you have Docker installed, this whole stack spins up with a single docker compose up. Create a directory and add these files:\ndocker-compose.yml services: litellm: image: ghcr.io/berriai/litellm:main-stable ports: - \u0026#34;4000:4000\u0026#34; volumes: - ./litellm-config.yaml:/app/config.yaml command: [\u0026#34;--config=/app/config.yaml\u0026#34;] environment: DATABASE_URL: \u0026#34;postgresql://llmproxy:dbpassword@litellm-db:5432/litellm\u0026#34; STORE_MODEL_IN_DB: \u0026#34;True\u0026#34; env_file: - .env depends_on: litellm-db: condition: service_healthy langfuse: condition: service_healthy litellm-db: image: postgres:17 environment: POSTGRES_DB: litellm POSTGRES_USER: llmproxy POSTGRES_PASSWORD: dbpassword volumes: - litellm_db_data:/var/lib/postgresql/data healthcheck: test: [\u0026#34;CMD-SHELL\u0026#34;, \u0026#34;pg_isready -U llmproxy -d litellm\u0026#34;] interval: 5s timeout: 5s retries: 5 langfuse: image: langfuse/langfuse:3 ports: - \u0026#34;3000:3000\u0026#34; environment: DATABASE_URL: postgresql://langfuse:dbpassword@langfuse-db:5432/langfuse NEXTAUTH_SECRET: generate-a-random-secret-here SALT: generate-a-random-salt-here NEXTAUTH_URL: http://localhost:3000 LANGFUSE_INIT_ORG_ID: my-org LANGFUSE_INIT_ORG_NAME: \u0026#34;My Org\u0026#34; LANGFUSE_INIT_PROJECT_ID: my-project LANGFUSE_INIT_PROJECT_NAME: \u0026#34;My Project\u0026#34; LANGFUSE_INIT_PROJECT_PUBLIC_KEY: pk-lf-local LANGFUSE_INIT_PROJECT_SECRET_KEY: sk-lf-local LANGFUSE_INIT_USER_EMAIL: admin@localhost LANGFUSE_INIT_USER_NAME: Admin LANGFUSE_INIT_USER_PASSWORD: adminpassword depends_on: langfuse-db: condition: service_healthy healthcheck: test: [\u0026#34;CMD\u0026#34;, \u0026#34;wget\u0026#34;, \u0026#34;--spider\u0026#34;, \u0026#34;-q\u0026#34;, \u0026#34;http://localhost:3000/api/public/health\u0026#34;] interval: 5s timeout: 5s retries: 5 langfuse-db: image: postgres:17 environment: POSTGRES_DB: langfuse POSTGRES_USER: langfuse POSTGRES_PASSWORD: dbpassword volumes: - langfuse_db_data:/var/lib/postgresql/data healthcheck: test: [\u0026#34;CMD-SHELL\u0026#34;, \u0026#34;pg_isready -U langfuse -d langfuse\u0026#34;] interval: 5s timeout: 5s retries: 5 volumes: litellm_db_data: langfuse_db_data: litellm-config.yaml model_list: - model_name: claude-opus-4-5 litellm_params: model: anthropic/claude-opus-4-5-20251101 api_key: os.environ/ANTHROPIC_API_KEY additional_drop_params: [\u0026#34;store\u0026#34;] - model_name: gpt-5.2 litellm_params: model: openai/gpt-5.2 api_key: os.environ/OPENAI_API_KEY additional_drop_params: [\u0026#34;store\u0026#34;] - model_name: gemini-3-pro litellm_params: model: gemini/gemini-3-pro-preview api_key: os.environ/GEMINI_API_KEY additional_drop_params: [\u0026#34;store\u0026#34;] - model_name: gemini-3-flash litellm_params: model: gemini/gemini-3-flash-preview api_key: os.environ/GEMINI_API_KEY additional_drop_params: [\u0026#34;store\u0026#34;] litellm_settings: drop_params: true success_callback: [\u0026#34;langfuse\u0026#34;] failure_callback: [\u0026#34;langfuse\u0026#34;] .env # LLM Provider Keys ANTHROPIC_API_KEY=sk-ant-... OPENAI_API_KEY=sk-... GEMINI_API_KEY=... # LiteLLM LITELLM_MASTER_KEY=sk-litellm-your-master-key # LangFuse connection (matches LANGFUSE_INIT values above) LANGFUSE_PUBLIC_KEY=pk-lf-local LANGFUSE_SECRET_KEY=sk-lf-local LANGFUSE_HOST=http://langfuse:3000 Start It Up docker compose up -d After about 30 seconds:\nLiteLLM proxy: http://localhost:4000 LangFuse UI: http://localhost:3000 (login: admin@localhost / adminpassword) Pointing Your Agent at LiteLLM For OpenClaw (or any OpenAI-compatible client), point it at LiteLLM instead of the provider directly. Add a new model (or multiple models) to your OpenClaw config, setting the base URL to http://localhost:4000 and use model names like claude-opus-4-5 (matching your litellm-config.yaml).\n... \u0026#34;models\u0026#34;: { \u0026#34;providers\u0026#34;: { \u0026#34;litellm\u0026#34;: { \u0026#34;baseUrl\u0026#34;: \u0026#34;http://localhost:4000/v1\u0026#34;, \u0026#34;apiKey\u0026#34;: \u0026#34;sk-....\u0026#34;, \u0026#34;api\u0026#34;: \u0026#34;openai-completions\u0026#34;, \u0026#34;authHeader\u0026#34;: true, \u0026#34;models\u0026#34;: [ { \u0026#34;id\u0026#34;: \u0026#34;claude-opus-4-5\u0026#34;, \u0026#34;name\u0026#34;: \u0026#34;Claude Opus 4.5 (via LiteLLM)\u0026#34;, \u0026#34;reasoning\u0026#34;: false, \u0026#34;input\u0026#34;: [ \u0026#34;text\u0026#34;, \u0026#34;image\u0026#34; ], \u0026#34;cost\u0026#34;: { \u0026#34;input\u0026#34;: 15, \u0026#34;output\u0026#34;: 75, \u0026#34;cacheRead\u0026#34;: 1.5, \u0026#34;cacheWrite\u0026#34;: 18.75 }, \u0026#34;contextWindow\u0026#34;: 200000, \u0026#34;maxTokens\u0026#34;: 32000, \u0026#34;compat\u0026#34;: { \u0026#34;supportsStore\u0026#34;: false } }, ... You then need to add the models to your agent\u0026rsquo;s model list. I add to my default agent as I am not using multi-agent.\n\u0026#34;agents\u0026#34;: { \u0026#34;defaults\u0026#34;: { \u0026#34;model\u0026#34;: { \u0026#34;primary\u0026#34;: \u0026#34;litellm/claude-opus-4-5\u0026#34;, }, \u0026#34;models\u0026#34;: { \u0026#34;litellm/claude-opus-4-5\u0026#34;: { \u0026#34;alias\u0026#34;: \u0026#34;ll-opus\u0026#34; }, \u0026#34;anthropic/claude-opus-4-5\u0026#34;: { \u0026#34;alias\u0026#34;: \u0026#34;opus\u0026#34; }, ... You can have as many models and providers as you want. The default will be whatever value is set with primary.\nWith the default model set as in the config above, or if you change to a LiteLLM powered model in chat with something like /models ll-opus, every API call will route through LiteLLM and gets logged to LangFuse.\nLangFuse page showing recent traces.\nWhat You Get Once traffic is flowing, LangFuse shows:\nTraces — Every conversation as a tree of API calls. See the full input/output, token counts, and latency for each step.\nCosts — Per-call pricing based on model and token usage. Aggregate by day, week, or session.\nDebugging — The full system prompt, tool calls, and model response.\nAn OpenClaw trace in LangFuse.\n","permalink":"https://cashwilliams.com/litellm-langfuse-observability/","summary":"\u003cp\u003eEarly in \u003ca href=\"https://github.com/openclaw/openclaw\"\u003eOpenClaw\u003c/a\u003e days (it was called Clawdis at the time, not even Clawdbot yet), everyone was noticing token spend was much higher than expected. I wanted to debug, so needed a way to see what was actually happening. How long is the system prompt? How many API calls per conversation? What\u0026rsquo;s the token breakdown?\u003c/p\u003e\n\u003cp\u003eI\u0026rsquo;ve used the LiteLLM -\u0026gt; LangFuse pattern before, so I quickly setup OpenClaw to point to a new instance of it. These are rough notes on how to do that.\u003c/p\u003e","title":"LiteLLM + LangFuse: Observability for AI Agents"},{"content":"I\u0026rsquo;ve made the hard decision to move off Drupal. After 10+ years, the old site served me well, but it was time for something new and simpler.\nI was inspired by a post by Dave Kiss where he detailed setting up and migrating his new site only through chatting with an AI bot via Telegram. Since I\u0026rsquo;ve already been running the same AI bot, I was inspired by the simplicity of both the process and lack of maintenance.\nSince I like to write everything in Markdown anyway, being able to quickly edit a file locally and then just git push really sold me. I\u0026rsquo;d already considered trying to hook up my ClawdBot to my Drupal site, so it could make changes, do update, etc. However have a simple git flow just made more sense. The bot built this site without me having to hook up anything.\nIn about 30 minutes, I had a chat session with my bot via Discord, and this new site was spun up and deployed to Cloudflare Pages. The only manual step I had to take was to migrate my DNS.\nThis blog is now:\nMarkdown files in a Git repo Hugo for static site generation Cloudflare Pages for hosting That\u0026rsquo;s it. No database, no CMS, no maintenance headaches.\nMore to come.\n","permalink":"https://cashwilliams.com/hello-world/","summary":"\u003cp\u003eI\u0026rsquo;ve made the hard decision to move off Drupal. After 10+ years, the old site served me well, but it was time for something new and simpler.\u003c/p\u003e\n\u003cp\u003eI was inspired by \u003ca href=\"https://davekiss.com/blog/i-rebuilt-my-website-via-telegram/\"\u003ea post by Dave Kiss\u003c/a\u003e where he detailed setting up\nand migrating his new site only through chatting with an AI bot via Telegram. Since I\u0026rsquo;ve already been running \u003ca href=\"https://clawd.bot/\"\u003ethe same AI bot\u003c/a\u003e,\nI was inspired by the simplicity of both the process and lack of maintenance.\u003c/p\u003e","title":"Hello World"},{"content":"I like to remap my capslock key to control. In OSX there is a nice option built in, however different Linux distros and window managers all seem to have different methods. I\u0026rsquo;ve used some settings in window managers, but then the remap doesn\u0026rsquo;t always work, particularly when connecting a new bluetooth keyboard.\nThis method will update the keyboard config in Debian/Linux:\nsudo vi /etc/default/keyboard Add XKBOPTIONS=\u0026quot;ctrl:nocaps\u0026quot;, then run:\nsudo dpkg-reconfigure keyboard-configuration ","permalink":"https://cashwilliams.com/remap-capslock-control-debian-ubuntu/","summary":"\u003cp\u003eI like to remap my capslock key to control. In OSX there is a nice option built in, however different Linux distros and window managers all seem to have different methods. I\u0026rsquo;ve used some settings in window managers, but then the remap doesn\u0026rsquo;t always work, particularly when connecting a new bluetooth keyboard.\u003c/p\u003e\n\u003cp\u003eThis method will update the keyboard config in Debian/Linux:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003esudo vi /etc/default/keyboard\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eAdd \u003ccode\u003eXKBOPTIONS=\u0026quot;ctrl:nocaps\u0026quot;\u003c/code\u003e, then run:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003esudo dpkg-reconfigure keyboard-configuration\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e","title":"Remap CapsLock to Control in Debian/Ubuntu"},{"content":"When setting up a new linux machine, I found myself needing to set the gnome terminal to a terminal emulator I\u0026rsquo;d installed via Flatpak.\nSince the Flatpak install is local to my user, it does not make sense to use the update-alternatives command, which is global.\nInstead, gsettings can be used to set a default Gnome application for the user.\nHowever, Flatpak apps don\u0026rsquo;t appear to be available in the user\u0026rsquo;s PATH, which is confusing.\nRecent Flatpak versions will however put a symlink to the executable in ~/.local/share/flatpak/exports/bin/. Here the applications installed by the user will be listed.\nIn my case, the following command solved the issue:\ngsettings set org.gnome.desktop.default-applications.terminal exec /home/cash/.local/share/flatpak/exports/bin/org.wezfurlong.wezterm ","permalink":"https://cashwilliams.com/setting-gnome-terminal-flatpak-app/","summary":"\u003cp\u003eWhen setting up a new linux machine, I found myself needing to set the gnome terminal to a terminal emulator I\u0026rsquo;d installed via Flatpak.\u003c/p\u003e\n\u003cp\u003eSince the Flatpak install is local to my user, it does not make sense to use the \u003ccode\u003eupdate-alternatives\u003c/code\u003e command, which is global.\u003c/p\u003e\n\u003cp\u003eInstead, \u003ccode\u003egsettings\u003c/code\u003e can be used to set a default Gnome application for the user.\u003c/p\u003e\n\u003cp\u003eHowever, Flatpak apps don\u0026rsquo;t appear to be available in the user\u0026rsquo;s PATH, which is confusing.\u003c/p\u003e","title":"Setting Gnome Terminal to Flatpak App"},{"content":"Continuing my speed challenge series against Dri.es, in this post I\u0026rsquo;m going to focus on removing unneeded browser requests.\nWhen rebuilding this site, shipping it quickly was the focus. With this in mind, I used the Bootstrap theme to put together a quick sub-theme with most of its default settings. This produces a decent looking site rather quickly, but it also comes at a frontend performance cost.\nWaterfall view of cashwilliams.com (actually the staging version of this site).\nI used WebPageTest.org to generate a waterfall view of a page load. If you aren\u0026rsquo;t familiar with waterfall views, Nooshu.com has a very in depth article explaining how to read them.\nThe waterfall view shows there are 10 total requests during the page load. The first step was to identify each of these requests and determine a way to remove them.\nRequest #6, #7, and #8 are all coming from jsdelivr.net, which is the default behavior of the Bootstrap theme. Request #9 is coming from fonts.googleapis.com, and I chose not to use this font at all by overriding it in my sub-theme\u0026rsquo;s CSS. Since the CSS file requesting the font is coming from the jsdelivr.net CDN, I couldn\u0026rsquo;t remove the reference to it. The Cost of the Connection It is interesting to take a look at the specific timing around each connection. WebPageTest.org also renders a table breaking down the different connection stages.\nThe request details view of homepage load for cashwilliams.com.\nThe chart provides some interesting details into the cost of each new request. Request #1 has a DNS lookup time of 27 ms, then a connection handshake of 32 ms, and a SSL negotiation of 74 ms. This means the connection cost of requesting a file from the domain cashwilliams.com for the first time is 133ms.\nOnce the initial request is downloaded and parsed, the browser requests the next four files from the same domain in parallel (this is reflected in the waterfall view above as well). The key point here is the connection cost for requests #2 through #5 is zero.\nThe connection cost comes back again at request #6 as this is from a new domain. The DNS lookup time in this case is a full 132 ms. Following that the connection handshake of 30 ms and the new SSL negotiation of 68 ms brings the total connection cost for request #6 to 230 ms. Almost a fourth of the total page load time is the browser attempting to make this new initial connection to the jsdelivr.net server.\nThe important takeaway here is, with these standard file sizes, the time to download the file itself is negligible compared to the time it takes to initiate the connection to request it.\nAddressing the Issues Resolving these issues was a straight forward, but lengthy process. I installed a SASS compiler and added both Bootstrap and the Yeti Bootswatch files into the main SASS include file. Then it was possible to remove request #9 for the default font included in the Yeti theme (rather than just overriding it with additional CSS in a later file), and adjust a few other default variables to maintain the look of the existing site.\nEditing the default variables in Acquia\u0026rsquo;s web based RemoteIDE tool.\nNow that a single custom CSS file is generated for the sub-theme, Drupal\u0026rsquo;s aggregation can compile it in with the existing CSS files. This effectively folds requests #6 and #7 into the requests #2 and #3.\nI also adjusted the sub-theme\u0026rsquo;s library config to point to the local copy of the Bootstrap javascript files. This places the contents of request #8 within the existing files served from requests #4 or #5.\nThe Results Running the WebPageTest.org test again with these deployed changes returns a much cleaner version of the connection details table.\nRequest Details for cashwilliams.com after initial optimization.\nThe screenshot shows only one connection is made for the entire page load, which has a 130 ms connection cost.\nThe final evaluation of this change is to check in with the official metric as measured by SpeedCurve.\nA significant drop in Start Render time after removing unneeded requests.\nThe Start Render time has dropped significantly, from around 1.4 seconds to just over 1 second. This is a great improvement from a single optimization pass.\n","permalink":"https://cashwilliams.com/website-performance-removing-unneeded-requests/","summary":"\u003cp\u003eContinuing \u003ca href=\"/website-performance-can-i-beat-dries\"\u003emy speed challenge\u003c/a\u003e series against \u003ca href=\"https://dri.es\"\u003eDri.es\u003c/a\u003e, in this post I\u0026rsquo;m going to focus on removing unneeded browser requests.\u003c/p\u003e\n\u003cp\u003eWhen rebuilding this site, shipping it quickly was the focus. With this in mind, I used the \u003ca href=\"https://www.drupal.org/project/bootstrap\"\u003eBootstrap theme\u003c/a\u003e to put together a quick sub-theme with most of its default settings. This produces a decent looking site rather quickly, but it also comes at a frontend performance cost.\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"Waterfall view of page load\" loading=\"lazy\" src=\"/website-performance-removing-unneeded-requests/waterfall_load.png\"\u003e\n\u003cem\u003eWaterfall view of cashwilliams.com (actually the staging version of this site).\u003c/em\u003e\u003c/p\u003e","title":"Website Performance - Removing Unneeded Requests"},{"content":"This post is an introduction into a series of posts deep diving into optimizing Drupal sites for browser performance.\nShortly after I rebuilt this site, I was reading a blog post on dri.es and noticed Dries has really gone out of his way to optimize the performance and size of that site. If you haven\u0026rsquo;t looked lately, it\u0026rsquo;s worth taking a look to see how lightweight and fast that site is. I thought it\u0026rsquo;d be a fun challenge to see if I can make this site even smaller and faster!\nBackground One of the goals of building this site was to keep it simple and performant. More and more websites are becoming way too complex and \u0026lsquo;bloated\u0026rsquo;, and I don\u0026rsquo;t want to contribute to this trend.\nFirst though, this site is empty, so why do I even have this site? I don\u0026rsquo;t blog much, nor do I intend to. I really only use this site as a live test bed for demos, experiments, and evaluating different changes at Acquia. (Also everyone wants to own their-name-dot-com, and I might as well have something running on it.)\nSecond, one of the tenants of security is to minimize your attack surface. For this reason I really try not to add many modules to the sites I build. As of this writing this site only has a few Drupal 8 core modules, one custom module, one base theme, and a custom theme.\nLastly this site doesn\u0026rsquo;t really need to do much, other than render a few pages and images, to serve its purpose. This makes it perfect for a CDN, and therefore backend server performance isn\u0026rsquo;t really an issue. With respect to this challenge, everything will be about download size and browser rendering performance.\nThe Challenge In order to have a challenge, rules of the game must be defined. In this case there needs be some metric that will be used and measured to be able to independently declare success or failure.\nIt doesn\u0026rsquo;t make sense to focus on the speed of the page response from Drupal. Both sites are blogs with anonymous users, and offloading content caching to the same CDN. Just trying to measure something like the Time To First Byte (TTFB) in a blunt way like the following would be both boring and fruitless.\n$ time curl -s https://dri.es \u0026gt; /dev/null real 0m0.188s user 0m0.020s sys 0m0.020s $ time curl -s https://cashwilliams.com \u0026gt; /dev/null real 0m0.181s user 0m0.021s sys 0m0.016s Real users don\u0026rsquo;t actually care about TTFB. Users expect a website to load and render as fast as possible, regardless of what bottlenecks might slow that process. This is referred to as perceived performance. Perceived performance can be hard to measure as by definition it\u0026rsquo;s based on a user\u0026rsquo;s perception, and can even be tricked with things like lazy loading and preloading.\nWith this in mind, the metric I\u0026rsquo;ll be using is SpeedIndex, which can be measured using tools like webpagetest.org and is defined as:\nThe Speed Index metric [\u0026hellip;] measures how quickly the page contents are visually populated (where lower numbers are better). It is particularly useful for comparing experiences of pages against each other (before/after optimizing, my site vs competitor, etc) and should be used in combination with the other metrics (load time, start render, etc) to better understand a site\u0026rsquo;s performance.\nSo the goal of this challenge is simple - can I make this site\u0026rsquo;s SpeedIndex lower than dri.es SpeedIndex?\nMeasuring and Baseline There are a few ways I could measure both this site\u0026rsquo;s and dri.es SpeedIndexes. In order to easily provide tracking over time, I\u0026rsquo;m going to use a tool called SpeedCurve which is purpose built for this task. I\u0026rsquo;ve used and recommended it many times in the past when delivering performance audits for clients at Acquia. Most importantly its base metric is SpeedIndex.\nI registered for a new free trial account and configured SpeedCurve to track both sites, and now we have a baseline to work from.\nSpeedCurve\u0026rsquo;s visualization of cashwilliams.com and dri.es SpeedIndex over three days.\nCurrently this site\u0026rsquo;s SpeedIndex is 0.8 seconds, which is decent, and only 0.3 seconds slower than Dries\u0026rsquo; site. However with these small numbers, this also means the site is 60% slower, which is a big gap to overcome.\nTo Come In future posts I\u0026rsquo;ll address some simple low-hanging fruit issues and measure their effects, and then dive deeper into specific browser issues and eventually get into some (probably unneeded) micro optimization to see if I can get a SpeedIndex value lower than dri.es.\n","permalink":"https://cashwilliams.com/website-performance-can-i-beat-dries/","summary":"\u003cp\u003eThis post is an introduction into a series of posts deep diving into optimizing Drupal sites for browser performance.\u003c/p\u003e\n\u003cp\u003eShortly after I rebuilt this site, I was reading a blog post on \u003ca href=\"https://dri.es/\"\u003edri.es\u003c/a\u003e and noticed Dries has really gone out of his way to optimize the performance and size of that site. If you haven\u0026rsquo;t looked lately, it\u0026rsquo;s worth taking a look to see how lightweight and fast that site is. I thought it\u0026rsquo;d be a fun challenge to see if I can make this site even smaller and faster!\u003c/p\u003e","title":"Website Performance - Can I Beat Dries?"},{"content":"I wanted to experiment with the new Sudo security bug recently released (CVE-2019-14287), so I created a quick Docker container to spin up an environment with different users and a vulnerable version. I posted the code for this on GitHub.\nThis container can be run with:\ndocker run -ti cashwilliams/cve-2019-14287-demo Configuration The container has three real users:\nroot alice bob The alice user is configured to have the ability to run any command as any other user (in this case bob as it is the only other user) using sudo -u(user) (command), however is restricted from running commands as root. This is configured in the /etc/sudoers file at the end using:\nalice ALL=(ALL,!root) NOPASSWD: ALL You can try to run commands as bob, such as opening a shell, with the following:\nsudo -ubob bash However, if you attempt to run a command as root, you will be prompted for a password which is unknown (and the command would fail anyway). This is by design and shows the sudo command working properly.\nCVE-2019-14287 The \u0026ldquo;minus_1_uid\u0026rdquo; bug within sudo was published on October 14, 2019 and demonstrates an issue with how the user argument is interpreted.\nIn this case alice can run commands as the root user, such as opening a shell, with the following:\nsudo -u#-1 bash This bypasses the restriction and gives alice root access despite the sudoers configuration explicitly denying it.\n","permalink":"https://cashwilliams.com/cve-2019-14287-demo-container/","summary":"\u003cp\u003eI wanted to experiment with the new Sudo security bug recently released (\u003ca href=\"https://www.sudo.ws/alerts/minus_1_uid.html\"\u003eCVE-2019-14287\u003c/a\u003e), so I created a quick Docker container to spin up an environment with different users and a vulnerable version. I posted the code for this on \u003ca href=\"https://github.com/CashWilliams/CVE-2019-14287-demo\"\u003eGitHub\u003c/a\u003e.\u003c/p\u003e\n\u003cp\u003eThis container can be run with:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003edocker run -ti cashwilliams/cve-2019-14287-demo\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch2 id=\"configuration\"\u003eConfiguration\u003c/h2\u003e\n\u003cp\u003eThe container has three real users:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eroot\u003c/li\u003e\n\u003cli\u003ealice\u003c/li\u003e\n\u003cli\u003ebob\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eThe \u003ccode\u003ealice\u003c/code\u003e user is configured to have the ability to run any command as any other user (in this case \u003ccode\u003ebob\u003c/code\u003e as it is the only other user) using \u003ccode\u003esudo -u(user) (command)\u003c/code\u003e, however is restricted from running commands as root. This is configured in the \u003ccode\u003e/etc/sudoers\u003c/code\u003e file at the end using:\u003c/p\u003e","title":"CVE-2019-14287 Demo Container"},{"content":"I\u0026rsquo;m Cash Williams, a tech enthusiast and home automation nerd based in Texas.\nBy day, I work at Acquia. By night, I\u0026rsquo;m probably tinkering with my smart home setup, debugging some automation, or figuring out how to make my AI assistant do something new.\nThis blog is where I write about things I\u0026rsquo;m building, problems I\u0026rsquo;ve solved, and whatever else seems worth sharing.\n","permalink":"https://cashwilliams.com/about/","summary":"About me","title":"About"}]