20 Comments
User's avatar
Josh Devon's avatar

Great guide, just be careful with Skills! Here’s how we hijacked a skill with an invisible prompt inject: https://open.substack.com/pub/securetrajectories/p/claude-skill-hijack-invisible-sentence

Expand full comment
Jacob Bumgarner's avatar

Wonderful write up. thank you.

Can you expand on this part a bit?

> I write simple bash scripts that call claude -p “in /pathA change all refs from foo to bar” in parallel.

How do you prevent the agents from overwriting the code each is writing? Switching branches for each call?

Expand full comment
Shrivu Shankar's avatar

Good question! Often my prompt includes some folder scope and I just trust that they stick to that. Sometimes they won't and most of the time the Claudes will figure it out.

Expand full comment
carsten jørgensen's avatar

Thanks for a great post. Could you please elaborate on "My preferred alternative is to use Claude’s built-in Task(...) feature to spawn clones of the general agent." Exactly how do you do this?

Expand full comment
Shrivu Shankar's avatar

Hey! You can just ask Claude to use "tasks" or "task subagents" to solve the problem. It has a tool for this (I want to say literally called "RunTask").

Expand full comment
carsten jørgensen's avatar

Thanks for a great post. Could you please elaborate at bit more on this part: "My preferred alternative is to use Claude’s built-in Task(...) feature to spawn clones of the general agent." How exactly do you do that? Thanks.

Expand full comment
Lionel Martin's avatar

Thanks for sharing Shrivu, this is very helpful!

Can you please share more details about your custom planning tool built on the Claude Code SDK?

🙏

Expand full comment
Shrivu Shankar's avatar

Hoping to eventually write a post about it but at a high level it's a fork of https://github.com/humanlayer/humanlayer/blob/main/.claude/commands/create_plan.md

but via claude -p ....

It's via the SDK rather than a slash command for a bit more control of how claude is configured when running it (ie override system prompt) and so we can build the prompt at runtime.

Expand full comment
Lionel Martin's avatar

Ha! This is amazing. I’ve been experimenting with the same approach after seeing Dexter’s talks. Have a look at the README here https://github.com/li0nel/claude-loop

Wanted to port this to using the SDK instead but I’m not sure the SDK supports using the Anthropic subscriptions?

Also I haven’t found a good way to measure the quality of the planning

Expand full comment
Lionel Martin's avatar

Have you had good results with overriding the system prompt?

Expand full comment
Sollan's avatar

"If your CLI commands are complex and verbose, don’t write paragraphs of documentation to explain them. Instead, write a simple bash wrapper with a clear, intuitive API and document that."

what is a bash wrapper? I've written paragraphs of documentation explaining CLI's how do I simplify with a "bash wrapper"

Expand full comment
Shrivu Shankar's avatar

Hey! Had gemini write up a detailed example:

-----

A **bash wrapper** is a simple shell script (e.g., `my_script.sh`) that "wraps" one or more complex, verbose, or multi-step commands into a single, new, and simpler command.

The article's point is that it's much easier to teach an AI agent (like Claude Code) to use a simple, well-defined tool than it is to teach it to *read and understand* a long paragraph of instructions.

### 🏛️ The Contrived "Enterprise" Example

Let's imagine your company has a complex, verbose CLI for deploying a service to staging. It requires multiple steps, has many flags, and is easy to get wrong.

#### Before: The "Paragraph of Documentation" (The Bad Way)

This is what you're currently doing, which forces the AI to read and (hopefully) understand a complex procedure.

**Your `CLAUDE.md` might look like this:**

> ## How to Deploy to Staging

>

> To deploy a service to staging, you must follow this 4-step process.

>

> 1. First, you must build the service container. Use the `internal-cli` command:

> `internal-cli build --service-name=<service_name> --env=staging --dockerfile-path=./services/<service_name>/Dockerfile.staging`

>

> 2. After the build is successful, you must **find the image ID** from the output and use it to tag the image for our private registry:

> `internal-cli tag-image --image-id=<id_from_step_1> --tag=registry.corp.com/staging/<service_name>:latest`

>

> 3. Next, you must push the tagged image. **Never** use the `--force` flag:

> `internal-cli push-image --image-tag=registry.corp.com/staging/<service_name>:latest --registry-user=$REGISTRY_USER`

>

> 4. Finally, you can run the database migration. **Be careful\!** Always run with `--dry-run` first, but for the agent, just run the final command:

> `internal-cli run-db-migration --env=staging --service=<service_name> --schema-path=./db/migrations/`

>

> **Never** deploy a service named `payment-gateway` from the CLI; it must be done from the dashboard.

An AI agent will almost certainly fail this. It might miss a step, use the wrong variable, or get stuck on the negative constraints ("Never...").

-----

#### After: The "Bash Wrapper" (The Good Way)

Instead of *documenting* the complex process, you *automate* it in a new, simple script.

**1. You create a new file named `deploy_staging.sh`:**

This script becomes the simple "tool." You (the human) write this once and check it into your repository.

```bash

#!/bin/bash

#

# This is a wrapper script to simplify staging deployments.

# It combines all 4 steps into one command.

# Usage: ./deploy_staging.sh <service-name>

# Exit immediately if any command fails

set -e

# 1. Get the service name from the first argument

SERVICE_NAME=$1

if [ -z "$SERVICE_NAME" ]; then

echo "Error: Missing service name."

echo "Usage: ./deploy_staging.sh <service-name>"

exit 1

fi

# Hard-code the complex flags and logic inside the script

echo "--- Starting staging deploy for $SERVICE_NAME ---"

# Step 1 & 2 & 3: Build, Tag, and Push

# We combine these using internal logic.

echo "Building and pushing container..."

internal-cli build --service-name=$SERVICE_NAME --env=staging --dockerfile-path=./services/$SERVICE_NAME/Dockerfile.staging

internal-cli tag-image --service-name=$SERVICE_NAME --tag=registry.corp.com/staging/$SERVICE_NAME:latest

internal-cli push-image --image-tag=registry.corp.com/staging/$SERVICE_NAME:latest --registry-user=$REGISTRY_USER

# Step 4: Run migrations

echo "Running database migrations..."

internal-cli run-db-migration --env=staging --service=$SERVICE_NAME --schema-path=./db/migrations/

echo "--- Successfully deployed $SERVICE_NAME to staging! ---"

```

*(Don't forget to make it executable: `chmod +x deploy_staging.sh`)*

**2. Now, this is what the agent sees in `CLAUDE.md`:**

All that complex documentation is **deleted** and replaced with one simple, positive instruction.

> ## How to Deploy to Staging

>

> * To deploy a service to the staging environment, run the wrapper script:

> `./deploy_staging.sh <service-name>`

>

> * **Never** use this for the `payment-gateway` service.

Expand full comment
Andrew VanLoo's avatar

Great post. I use Claude Code a ton and fairly effectively, but I learned a few new tricks today. Thanks.

Expand full comment
Seth Miller's avatar

So good dude thx

Expand full comment
Brian's avatar

Any thoughts on plugins? I was surprised you didn't mention them.

Expand full comment
Shrivu Shankar's avatar

My understanding of plugins is that they just make it easy to import pre-built config files. For me all of this config is just custom/internally built so there hasn't been a need. I'm sure overtime there will be a lot more out of the box plugins that folks will prefer over reimplementing.

Expand full comment
Joey Musselman's avatar

Wow such a good article. So happy to have found your work, can’t wait to read your other articles!

Expand full comment
Bilgin Ibryam's avatar

A fantastic guide! 👏 👏 👏

Expand full comment
Daniel's avatar

I would love to see the catchup custom slash command, can you share it?

Expand full comment
Shrivu Shankar's avatar

```

# Catch Up on Branch Changes

This command reviews all changes between the current branch and main, analyzes key files, and provides a comprehensive summary to help you catch up on what's been done.

## Steps:

1. Get the current branch name and verify we're not on main

2. Run `git fetch origin main` to ensure we have the latest main branch

3. Run `git diff main...HEAD --name-only` to get list of changed files

4. Run `git diff main...HEAD --stat` to get a summary of changes

5. Run `git log main..HEAD --oneline` to get the commit history for this branch

6. Read and analyze the most important changed files (prioritize: manifests, configs, core logic files)

7. Provide a high-level summary including:

- Branch purpose and scope of changes

- Key files modified and their significance

- Potential impact areas

- Any notable patterns or architectural changes

- Suggested review focus areas

```

Expand full comment